AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/09/03 13:17 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "KCOV",
    "KCOV_DATAFLOW_ARGS",
    "KCOV_DATAFLOW_RET",
    "KCOV_DATAFLOW_INSTRUMENT_ALL",
    "KCOV_DATAFLOW_NO_INLINE",
    "KCOV_ENABLE_COMPARISONS",
    "DEBUG_FS",
    "DEBUG_INFO"
  ],
  "FocusSymbols": [
    "kcov_df_ioctl",
    "kcov_df_mmap",
    "kcov_df_open",
    "kcov_df_close",
    "kcov_df_remote_start",
    "kcov_df_remote_stop",
    "kcov_df_trace_cmp",
    "__sanitizer_cov_trace_args",
    "__sanitizer_cov_trace_ret",
    "kcov_dataflow_task_exit",
    "kcov_dataflow_task_init",
    "kcov_df_scratch_shrink_scan",
    "kcov_df_scratch_shrink_count",
    "write_comp_data"
  ],
  "KMSANReasoning": "The patch introduces KCOV-Dataflow, adding new infrastructure to capture function arguments, return values, and comparison operands into mmap-able per-task and remote buffers.\n\nAnalysis for uninitialized memory risks:\n1. Heap objects (`struct kcov_dataflow`, `struct kcov_df_remote`) are allocated using `kzalloc_obj()`, ensuring all fields are zero-initialized.\n2. The user-accessible dataflow buffer is allocated via `vmalloc_user()`, which automatically zeroes memory pages.\n3. In data collection callbacks (`kcov_df_write()`, `kcov_df_trace_cmp()`), all record fields, headers, and values are explicitly initialized (`val = 0`, etc.) before being populated via safe accessors (`copy_from_kernel_nofault()`, `get_kernel_nofault()`).\n4. Per-task dataflow fields in `task_struct` are explicitly initialized in `kcov_dataflow_task_init()`.\n5. No uninitialized memory or struct padding is copied to user space or evaluated in branching decisions.\n6. `kernel/kcov_dataflow.c` explicitly disables KMSAN instrumentation (`KMSAN_SANITIZE_kcov_dataflow.o := n`).\n7. Potential concurrency, bounds, and refcounting issues fall strictly under the domain of KASAN, LOCKDEP, and standard kernel debugging facilities.\n\nTherefore, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces KCOV-Dataflow, adding a new debugfs interface (/sys/kernel/debug/kcov_dataflow), ioctls (KCOV_DF_INIT_TRACK, KCOV_DF_ENABLE, KCOV_DF_DISABLE, KCOV_DF_REMOTE_ENABLE, KCOV_DF_REMOTE_DISABLE), mmap, remote kworker tracing, task lifecycle tracking in fork/exit, memory shrinker management, and comparison fan-out. This introduces significant new kernel code and state management in reachable subsystems that warrants fuzzing.",
  "WorthFuzzing": true
}

1/1 2026/09/03 13:17 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit add0fd47998ebfbe54b3752a7ef1aa9d7ee4fc05\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Thu Sep 3 13:17:20 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst\nindex 59cbb77b33ff4..541c58cc65ea5 100644\n--- a/Documentation/dev-tools/index.rst\n+++ b/Documentation/dev-tools/index.rst\n@@ -24,6 +24,7 @@ Documentation/process/debugging/index.rst\n    context-analysis\n    sparse\n    kcov\n+   kcov-dataflow\n    gcov\n    kasan\n    kmsan\ndiff --git a/Documentation/dev-tools/kcov-dataflow.rst b/Documentation/dev-tools/kcov-dataflow.rst\nnew file mode 100644\nindex 0000000000000..4c023032fea00\n--- /dev/null\n+++ b/Documentation/dev-tools/kcov-dataflow.rst\n@@ -0,0 +1,449 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow: function argument and return value extraction\n+=============================================================\n+\n+KCOV-Dataflow captures function arguments and return values, including\n+automatic struct field decomposition, at instrumented kernel function\n+boundaries. It provides per-task, lock-free ring buffers accessible via\n+``mmap()``, enabling data-flow-aware fuzzing and post-mortem contract\n+verification.\n+\n+Unlike KCOV's ``trace-pc`` which reports *which* code executed,\n+KCOV-Dataflow reports *what values* were passed and returned. This is\n+a completely separate device from ``/sys/kernel/debug/kcov``.\n+\n+Prerequisites\n+-------------\n+\n+KCOV-Dataflow requires Clang/LLVM with the ``trace-args`` and\n+``trace-ret`` SanitizerCoverage extensions. Standard (unpatched)\n+compilers will not expose these Kconfig options.\n+\n+To enable KCOV-Dataflow, configure the kernel with::\n+\n+        CONFIG_KCOV=y\n+        CONFIG_KCOV_DATAFLOW_ARGS=y\n+        CONFIG_KCOV_DATAFLOW_RET=y\n+\n+Optional: instrument the entire kernel (significant overhead)::\n+\n+        CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y\n+\n+Coverage data becomes accessible once debugfs is mounted::\n+\n+        mount -t debugfs none /sys/kernel/debug\n+\n+Per-module instrumentation\n+--------------------------\n+\n+To instrument a specific module, add to its Makefile::\n+\n+        KCOV_DATAFLOW_my_module.o := y\n+\n+For example, to instrument the Android binder driver::\n+\n+        # drivers/android/Makefile\n+        KCOV_DATAFLOW_binder.o := y\n+        KCOV_DATAFLOW_binder_alloc.o := y\n+\n+To instrument an entire directory, set the variable without a filename::\n+\n+        # fs/Makefile\n+        KCOV_DATAFLOW := y\n+\n+The build system automatically adds the required compiler flags\n+(``-fsanitize-coverage=trace-args,trace-ret``). Debug info is provided\n+by ``CONFIG_DEBUG_INFO`` which is a Kconfig dependency.\n+\n+Data collection\n+---------------\n+\n+The following program demonstrates how to collect function argument and\n+return value data for a single syscall:\n+\n+.. code-block:: c\n+\n+    #include \u003cstdio.h\u003e\n+    #include \u003cstdint.h\u003e\n+    #include \u003cstdlib.h\u003e\n+    #include \u003csys/types.h\u003e\n+    #include \u003csys/ioctl.h\u003e\n+    #include \u003csys/mman.h\u003e\n+    #include \u003cunistd.h\u003e\n+    #include \u003cfcntl.h\u003e\n+\n+    #include \u003clinux/kcov_dataflow.h\u003e   /* ioctls, record layout, helpers */\n+    #define BUF_SIZE            (1 \u003c\u003c 20)  /* 1M words = 8MB */\n+\n+    int main(void)\n+    {\n+        int fd;\n+        uint64_t *buf, n, i;\n+\n+        fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+        if (fd == -1)\n+            perror(\"open\"), exit(1);\n+\n+        /* Allocate buffer (size in u64 words). */\n+        if (ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE))\n+            perror(\"ioctl(INIT)\"), exit(1);\n+\n+        /* Map the buffer into user space. */\n+        buf = (uint64_t *)mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+                               PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n+        if (buf == MAP_FAILED)\n+            perror(\"mmap\"), exit(1);\n+\n+        /* Enable data-flow collection for this task. */\n+        if (ioctl(fd, KCOV_DF_ENABLE, 0))\n+            perror(\"ioctl(ENABLE)\"), exit(1);\n+\n+        /* Reset counter. */\n+        __atomic_store_n(\u0026buf[0], 0, __ATOMIC_RELAXED);\n+\n+        /* === Trigger syscall(s) here === */\n+        read(-1, NULL, 0);\n+\n+        /* Read how many words were written. */\n+        n = __atomic_load_n(\u0026buf[0], __ATOMIC_RELAXED);\n+\n+        /* Parse TLV records. */\n+        i = 1;\n+        while (i + KCOV_DF_RECORD_HDR_WORDS \u003c= 1 + n) {\n+            uint64_t hdr      = buf[i];\n+            uint64_t pc       = buf[i + 1];   /* KASLR offset removed */\n+            uint64_t ptr      = buf[i + 2];   /* traced pointer (ENTRY/RET) */\n+            uint32_t type     = KCOV_DF_HDR_TYPE(hdr);\n+            uint32_t num_vals = KCOV_DF_HDR_NVALS(hdr);\n+            uint32_t seq      = KCOV_DF_HDR_SEQ(hdr);\n+            uint32_t arg_idx  = KCOV_DF_HDR_ARGIDX(hdr);\n+            uint32_t size     = KCOV_DF_HDR_SIZE(hdr);\n+\n+            if (!num_vals || (type != KCOV_DF_TYPE_ENTRY \u0026\u0026\n+                              type != KCOV_DF_TYPE_RET \u0026\u0026\n+                              type != KCOV_DF_TYPE_CMP)) {\n+                i++;    /* garbage (e.g. reset mid-run): resync */\n+                continue;\n+            }\n+            if (type != KCOV_DF_TYPE_CMP)\n+                printf(\"[%s] seq=%u pc=0x%lx ptr=0x%lx arg_idx=%u size=%u val=0x%lx\\n\",\n+                       type == KCOV_DF_TYPE_ENTRY ? \"ENTRY\" : \"RET\",\n+                       seq, pc, ptr, arg_idx, size, buf[i + 3]);\n+            i += KCOV_DF_RECORD_WORDS(num_vals);\n+        }\n+\n+        if (ioctl(fd, KCOV_DF_DISABLE, 0))\n+            perror(\"ioctl(DISABLE)\"), exit(1);\n+\n+        munmap(buf, BUF_SIZE * sizeof(uint64_t));\n+        close(fd);\n+        return 0;\n+    }\n+\n+Ring buffer format\n+------------------\n+\n+The buffer is an array of ``u64`` words::\n+\n+        buf[0]: atomic counter -- total words written\n+\n+Each record occupies 3 + N words:\n+\n+.. list-table::\n+   :header-rows: 1\n+\n+   * - Offset\n+     - Field\n+     - Description\n+   * - 0\n+     - header\n+     - bits[63:56] = arg_idx (0 for return), bits[55:48] = size in bytes\n+       (clamped to 255), bits[47:32] = num_vals (\u003e= 1),\n+       bits[31:28] = type: ``KCOV_DF_TYPE_ENTRY`` (0xE),\n+       ``KCOV_DF_TYPE_RET`` (0xF) or ``KCOV_DF_TYPE_CMP`` (0xC),\n+       bits[23:0] = sequence number\n+   * - 1\n+     - pc\n+     - Instrumented function address with the KASLR offset removed (same\n+       as the PCs mainline kcov records), so it can be symbolized against\n+       vmlinux; add the runtime offset back for ``/proc/kallsyms``\n+   * - 2\n+     - ptr / cmp_type\n+     - ENTRY/RET: the full 64-bit traced pointer (may be NULL/ERR_PTR, in\n+       which case the values are ``0xBADADD85``). CMP: the comparison\n+       type, ``KCOV_CMP_SIZE()``/``KCOV_CMP_CONST`` bits from linux/kcov.h\n+   * - 3..3+num_vals\n+     - values\n+     - Struct field values, a single scalar, or the two CMP operands\n+\n+``area[0]`` never exceeds the buffer size minus one and every counted word\n+has been written, so a consumer that walks ``area[0]`` words never leaves\n+its mapping. All of the above is defined in ``include/uapi/linux/kcov_dataflow.h``\n+(``KCOV_DF_HDR_*()``, ``KCOV_DF_RECORD_WORDS()``).\n+\n+Magic values:\n+\n+- ``0xBADADD85``: field read failed (pointer was invalid/freed/poisoned)\n+\n+Safety\n+------\n+\n+- Callbacks are ``notrace``, ``__no_sanitize_coverage``, ``noinline``\n+  to prevent recursion.\n+- All pointer reads use ``copy_from_kernel_nofault()`` -- survives\n+  freed, poisoned, or unmapped memory.\n+- An ``in_task()`` guard rejects calls from hardirq/softirq/NMI context,\n+  preventing reentrant buffer corruption.\n+- No ``printk`` or allocation in the data path.\n+- When not enabled for a task, overhead is a single boolean check.\n+\n+Ioctl interface\n+---------------\n+\n+.. list-table::\n+   :header-rows: 1\n+\n+   * - Command\n+     - Value\n+     - Description\n+   * - KCOV_DF_INIT_TRACK\n+     - ``_IOR('d', 1, unsigned long)``\n+     - Allocate buffer (size in u64 words)\n+   * - KCOV_DF_ENABLE\n+     - ``_IO('d', 100)``\n+     - Start collection for current task\n+   * - KCOV_DF_DISABLE\n+     - ``_IO('d', 101)``\n+     - Stop collection\n+   * - KCOV_DF_REMOTE_ENABLE\n+     - ``_IOW('d', 102, __u64)`` -- argument is a pointer to the handle\n+     - Publish buffer for kworker/kthread remote capture\n+   * - KCOV_DF_REMOTE_DISABLE\n+     - ``_IO('d', 103)``\n+     - Unpublish buffer from remote capture\n+\n+Compatibility\n+-------------\n+\n+KCOV-Dataflow is completely independent from legacy KCOV:\n+\n+- Separate device: ``/sys/kernel/debug/kcov_dataflow``\n+- Separate ioctl namespace (``'d'`` vs ``'c'``)\n+- Separate per-task buffer\n+- Both can be used simultaneously without interference\n+- syzkaller and other KCOV users are unaffected\n+\n+Rust module support\n+-------------------\n+\n+Rust kernel modules are instrumented natively through the build system.\n+The ``KCOV_DATAFLOW_\u003cmodule\u003e.o := y`` mechanism works identically for\n+Rust and C modules. The build system passes\n+``-Cllvm-args=-sanitizer-coverage-trace-args`` and\n+``-Cllvm-args=-sanitizer-coverage-trace-ret`` to rustc via\n+``RUSTFLAGS_KCOV_DATAFLOW``.\n+\n+Example Makefile for a Rust module::\n+\n+        obj-m := my_rust_module.o\n+        KCOV_DATAFLOW_my_rust_module.o := y\n+\n+Requires a rustc built against LLVM with trace-args/trace-ret support\n+and ``CONFIG_RUST=y`` in the kernel config.\n+\n+Selftests\n+---------\n+\n+Automated tests and visualization tools are in\n+``tools/testing/selftests/kcov_dataflow/``::\n+\n+        # Automated ioctl interface test (TAP output):\n+        make -C tools/testing/selftests/kcov_dataflow\n+        vng --user root --exec \\\n+          tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\n+\n+        # Load a test module and view captured records:\n+        make LLVM=1 CC=clang M=tools/testing/selftests/kcov_dataflow/eight_struct_args_c modules\n+        vng --user root --exec \\\n+          \"python3 tools/testing/selftests/kcov_dataflow/trigger-view.py \\\n+            eight_struct_args_c --ko \\\n+            tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.ko\"\n+\n+        # Binderfs ioctl capture test (requires CONFIG_ANDROID_BINDER_IPC):\n+        make -C tools/testing/selftests/kcov_dataflow/binderfs\n+        vng --user root --exec \\\n+          tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\n+\n+See ``tools/testing/selftests/kcov_dataflow/README.rst`` for details.\n+\n+Tracing child processes\n+-----------------------\n+\n+KCOV-Dataflow is per-task: after ``fork()``, the child does not inherit\n+the enabled state. To trace child processes, re-enable on the inherited\n+file descriptor in the child before ``exec()``. The ``mmap``'d buffer is\n+shared (``MAP_SHARED``), so both parent and child write to the same ring\n+buffer atomically.\n+\n+.. code-block:: c\n+\n+    #include \u003cstdio.h\u003e\n+    #include \u003cstdint.h\u003e\n+    #include \u003cstdlib.h\u003e\n+    #include \u003csys/ioctl.h\u003e\n+    #include \u003csys/mman.h\u003e\n+    #include \u003csys/wait.h\u003e\n+    #include \u003cunistd.h\u003e\n+    #include \u003cfcntl.h\u003e\n+\n+    #include \u003clinux/kcov_dataflow.h\u003e   /* ioctls, record layout, helpers */\n+    #define BUF_SIZE            (1 \u003c\u003c 20)\n+\n+    int main(int argc, char **argv)\n+    {\n+        int fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+        ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);\n+        uint64_t *buf = mmap(NULL, BUF_SIZE * 8,\n+                             PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n+\n+        /* Enable for parent task. */\n+        ioctl(fd, KCOV_DF_ENABLE, 0);\n+        __atomic_store_n(\u0026buf[0], 0, __ATOMIC_RELAXED);\n+\n+        pid_t pid = fork();\n+        if (pid == 0) {\n+            /*\n+             * Child: re-enable on inherited fd.\n+             * The shared mmap buffer receives records from both tasks.\n+             */\n+            ioctl(fd, KCOV_DF_ENABLE, 0);\n+            execvp(argv[1], \u0026argv[1]);\n+            _exit(1);\n+        }\n+\n+        waitpid(pid, NULL, 0);\n+        ioctl(fd, KCOV_DF_DISABLE, 0);\n+\n+        uint64_t n = __atomic_load_n(\u0026buf[0], __ATOMIC_RELAXED);\n+        printf(\"Captured %lu words from parent + child\\n\", n);\n+\n+        munmap(buf, BUF_SIZE * 8);\n+        close(fd);\n+        return 0;\n+    }\n+\n+Note: the child's ``ioctl(fd, KCOV_DF_ENABLE)`` will fail if the parent\n+has not yet called ``KCOV_DF_DISABLE``, because only one task can be\n+associated with a descriptor at a time. For true multi-process tracing,\n+open a separate ``kcov_dataflow`` fd per child, or disable in the parent\n+before the child enables (as shown above -- the parent is blocked in\n+``waitpid`` so it generates no records during that time anyway).\n+\n+Remote tracing (kworker/kthread)\n+--------------------------------\n+\n+To capture data from kernel threads (kworkers, kthreads) that are not\n+direct descendants of user space, use the remote API:\n+\n+1. User space allocates and publishes a buffer with ``KCOV_DF_REMOTE_ENABLE``\n+2. The kernel module calls ``kcov_df_remote_start()`` at work entry\n+3. The kernel module calls ``kcov_df_remote_stop()`` at work exit\n+4. User space reads the buffer and unpublishes with ``KCOV_DF_REMOTE_DISABLE``\n+\n+User space setup:\n+\n+.. code-block:: c\n+\n+    #include \u003cstdio.h\u003e\n+    #include \u003cstdint.h\u003e\n+    #include \u003csys/ioctl.h\u003e\n+    #include \u003csys/mman.h\u003e\n+    #include \u003cunistd.h\u003e\n+    #include \u003cfcntl.h\u003e\n+\n+    #include \u003clinux/kcov.h\u003e            /* kcov_remote_handle() */\n+    #include \u003clinux/kcov_dataflow.h\u003e\n+    #define BUF_SIZE                (1 \u003c\u003c 20)\n+\n+    int main(void)\n+    {\n+        int fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+        ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);\n+        uint64_t *buf = mmap(NULL, BUF_SIZE * 8,\n+                             PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n+        __atomic_store_n(\u0026buf[0], 0, __ATOMIC_RELAXED);\n+\n+        /*\n+         * Publish the buffer under a remote handle. The handle must be a\n+         * valid kcov_remote_handle() encoding (KCOV_SUBSYSTEM_COMMON with a\n+         * nonzero instance, or KCOV_SUBSYSTEM_USB) and is the value the\n+         * kernel side passes to kcov_df_remote_start(); one handle per fd,\n+         * and not while KCOV_DF_ENABLE is active on the same fd.\n+         */\n+        __u64 handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, 1);\n+        if (ioctl(fd, KCOV_DF_REMOTE_ENABLE, \u0026handle))\n+            perror(\"ioctl(REMOTE_ENABLE)\"), exit(1);\n+\n+        /* Trigger kworker activity (e.g., write to a file, ioctl). */\n+        /* ... */\n+        sleep(1);\n+\n+        /* Unpublish and read results. */\n+        ioctl(fd, KCOV_DF_REMOTE_DISABLE, 0);\n+\n+        uint64_t n = __atomic_load_n(\u0026buf[0], __ATOMIC_RELAXED);\n+        printf(\"Captured %lu words from kworker\\n\", n);\n+\n+        munmap(buf, BUF_SIZE * 8);\n+        close(fd);\n+        return 0;\n+    }\n+\n+Kernel module side (called from kworker context):\n+\n+.. code-block:: c\n+\n+    #include \u003clinux/kcov.h\u003e\n+\n+    void my_work_fn(struct work_struct *work)\n+    {\n+        kcov_df_remote_start();\n+        /* ... instrumented code runs here ... */\n+        kcov_df_remote_stop();\n+    }\n+\n+Only one buffer can be published at a time. ``kcov_df_remote_start()``\n+is a no-op if no buffer is published or if the current task already has\n+dataflow enabled.\n+\n+Limitations\n+-----------\n+\n+ABI argument mapping\n+    The LLVM pass maps IR-level arguments to source-level parameters using\n+    ``DILocalVariable`` debug records (``-g`` required). This correctly\n+    handles hidden ``sret`` pointers, struct decomposition into multiple\n+    registers, and C++ ``this`` pointers.\n+\n+    When debug info is absent or stripped, the pass falls back to positional\n+    indexing which may misattribute arguments in functions with ABI-inserted\n+    hidden parameters. The kernel is always built with ``-g``, so this\n+    limitation does not apply to kernel use.\n+\n+Struct-by-value reassembly\n+    When a small struct is passed by value and the ABI decomposes it into\n+    multiple scalar registers (e.g., ``struct { int x; int y; }`` as two\n+    ``i32`` values on x86_64), the pass reassembles the fragments into a\n+    stack slot. The struct field offsets are preserved, but if a field was\n+    entirely optimized away (no debug record), that slot contains zero.\n+\n+    In kernel code, structs are always passed by pointer, so this case\n+    does not arise.\n+\n+Optimized builds\n+    At ``-O2`` and above, LLVM may eliminate ``#dbg_value`` records for\n+    arguments that are dead or fully inlined. Such arguments will emit a\n+    trace with a null pointer (producing ``0xBADADD85`` in all field\n+    positions), indicating the argument existed but its value was\n+    unavailable at runtime.\ndiff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst\nindex 2fc53093752d1..7864b2e7fb476 100644\n--- a/Documentation/userspace-api/ioctl/ioctl-number.rst\n+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst\n@@ -240,6 +240,8 @@ Code  Seq#    Include File                                             Comments\n 'd'   00-FF  linux/char/drm/drm.h                                      conflict!\n 'd'   02-40  pcmcia/ds.h                                               conflict!\n 'd'   F0-FF  linux/digi1.h\n+'d'   01     uapi/linux/kcov_dataflow.h                                conflict!\n+'d'   64-67  uapi/linux/kcov_dataflow.h                                conflict!\n 'e'   all    linux/digi1.h                                             conflict!\n 'f'   00-1F  linux/ext2_fs.h                                           conflict!\n 'f'   00-1F  linux/ext3_fs.h                                           conflict!\ndiff --git a/MAINTAINERS b/MAINTAINERS\nindex a9245d827ddb6..057f4e14ff46e 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -14077,7 +14077,9 @@ B:\thttps://bugzilla.kernel.org/buglist.cgi?component=Sanitizers\u0026product=Memory%2\n F:\tDocumentation/dev-tools/kcov.rst\n F:\tinclude/linux/kcov.h\n F:\tinclude/uapi/linux/kcov.h\n+F:\tinclude/uapi/linux/kcov_dataflow.h\n F:\tkernel/kcov.c\n+F:\tkernel/kcov_dataflow.c\n F:\tscripts/Makefile.kcov\n \n KCSAN\ndiff --git a/include/linux/kcov.h b/include/linux/kcov.h\nindex 895b761b2db15..55e1405bc4bc4 100644\n--- a/include/linux/kcov.h\n+++ b/include/linux/kcov.h\n@@ -3,6 +3,7 @@\n #define _LINUX_KCOV_H\n \n #include \u003clinux/sched.h\u003e\n+#include \u003clinux/jump_label.h\u003e\n #include \u003cuapi/linux/kcov.h\u003e\n \n struct task_struct;\n@@ -28,6 +29,14 @@ enum kcov_mode {\n void kcov_task_init(struct task_struct *t);\n void kcov_task_exit(struct task_struct *t);\n \n+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)\n+void kcov_dataflow_task_init(struct task_struct *t);\n+void kcov_dataflow_task_exit(struct task_struct *t);\n+#else\n+static inline void kcov_dataflow_task_init(struct task_struct *t) {}\n+static inline void kcov_dataflow_task_exit(struct task_struct *t) {}\n+#endif\n+\n #define kcov_prepare_switch(t)\t\t\t\\\n do {\t\t\t\t\t\t\\\n \t(t)-\u003ekcov_mode |= KCOV_IN_CTXSW;\t\\\n@@ -43,6 +52,29 @@ void kcov_remote_start(u64 handle);\n void kcov_remote_stop(void);\n struct kcov_common_handle_id kcov_common_handle(void);\n \n+/*\n+ * Validate a remote handle: it must be a well-formed kcov_remote_handle()\n+ * encoding, and each caller states which subsystem/instance combinations it\n+ * accepts. Shared by KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE so both\n+ * collectors take handles from the same partitioned namespace.\n+ */\n+static inline bool kcov_check_handle(u64 handle, bool common_valid,\n+\t\t\t\t     bool uncommon_valid, bool zero_valid)\n+{\n+\tif (handle \u0026 ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))\n+\t\treturn false;\n+\tswitch (handle \u0026 KCOV_SUBSYSTEM_MASK) {\n+\tcase KCOV_SUBSYSTEM_COMMON:\n+\t\treturn (handle \u0026 KCOV_INSTANCE_MASK) ?\n+\t\t\tcommon_valid : zero_valid;\n+\tcase KCOV_SUBSYSTEM_USB:\n+\t\treturn uncommon_valid;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\treturn false;\n+}\n+\n static inline void kcov_remote_start_common(struct kcov_common_handle_id id)\n {\n \tkcov_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));\n@@ -107,4 +139,88 @@ static inline void kcov_remote_start_usb_softirq(u64 id) {}\n static inline void kcov_remote_stop_softirq(void) {}\n \n #endif /* CONFIG_KCOV */\n+\n+/*\n+ * kcov_dataflow remote API. The collector is a separate object from mainline\n+ * kcov and is only linked in when at least one of the two capture modes is\n+ * configured (see kernel/Makefile), so gate the declarations the same way\n+ * kcov_dataflow_task_init() above is gated; a caller that brackets a region for\n+ * both collectors then still builds on a KCOV-only config.\n+ */\n+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)\n+void kcov_df_remote_start(u64 handle);\n+void kcov_df_remote_stop(void);\n+#else\n+static inline void kcov_df_remote_start(u64 handle) {}\n+static inline void kcov_df_remote_stop(void) {}\n+#endif\n+\n+/*\n+ * Handle-typed wrapper mirroring kcov_remote_start_common(), so a subsystem that\n+ * already routes its mainline kcov remote sections by struct\n+ * kcov_common_handle_id can open a dataflow section on the very same handle\n+ * without knowing how it is encoded. The two collectors keep separate per-task\n+ * state and separate handle tables, so a section of each may be nested around\n+ * the same region; user space registers the identical handle value with\n+ * KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE to collect both.\n+ *\n+ * Unlike kcov_remote_start(), the dataflow section may only be opened from\n+ * sleepable task context: kcov_df_remote_start()/kcov_df_remote_stop() take a\n+ * mutex and may allocate or free the worker's scratch area. Both are no-ops in\n+ * softirq/hardirq context, so a softirq-bracketing call site collects no\n+ * dataflow records rather than misbehaving. A call site that is only\n+ * sometimes atomic (spinlock held, preemption or irqs disabled) must not use\n+ * this wrapper; CONFIG_DEBUG_ATOMIC_SLEEP reports such a caller.\n+ *\n+ * Without CONFIG_KCOV the handle carries no value (see struct\n+ * kcov_common_handle_id), and dataflow depends on KCOV, so this is a no-op.\n+ */\n+#ifdef CONFIG_KCOV\n+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)\n+{\n+\tkcov_df_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));\n+}\n+#else\n+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)\n+{\n+}\n+#endif\n+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) \u0026\u0026 \\\n+\t(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))\n+/*\n+ * CONFIG_KCOV_ENABLE_COMPARISONS provides ONE trace-cmp instrumentation shared by\n+ * mainline kcov and kcov-dataflow. kcov.c's __sanitizer_cov_trace_cmp*() callbacks\n+ * route each operand pair through kcov_trace_cmp() below, which fans it out:\n+ * mainline kcov always sees it (write_comp_data() records only when the task is\n+ * in KCOV_MODE_TRACE_CMP), and a task with a live dataflow session gets a copy in\n+ * its dataflow buffer as well. The two collectors are independent fds with no\n+ * cross-exclusion, so a task may collect for both at once, and a dataflow-side\n+ * drop (inert context, full buffer) never costs mainline kcov a record. kcov.c\n+ * never references the dataflow side, one cmp symbol feeds both collectors, and\n+ * there is no separate df_cmp symbol or compiler change.\n+ *\n+ * The dataflow branch is gated by a static key so that, while no dataflow session\n+ * is live, this whole-kernel hot path is a patched-out NOP that costs nothing on\n+ * top of mainline write_comp_data() (kcov_df_cmp_key is inc'd on dataflow enable\n+ * in kcov_dataflow.c).\n+ */\n+DECLARE_STATIC_KEY_FALSE(kcov_df_cmp_key);\n+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);\n+void kcov_df_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip);\n+static inline notrace void\n+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)\n+{\n+\twrite_comp_data(type, arg1, arg2, ip);\t\t\t/* mainline kcov */\n+\tif (static_branch_unlikely(\u0026kcov_df_cmp_key) \u0026\u0026 current-\u003ekcov_df_enabled)\n+\t\tkcov_df_trace_cmp(type, arg1, arg2, ip);\t/* kcov-dataflow */\n+}\n+#elif defined(CONFIG_KCOV_ENABLE_COMPARISONS)\n+/* Comparisons without a dataflow build: route straight to mainline kcov. */\n+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);\n+static inline notrace void\n+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)\n+{\n+\twrite_comp_data(type, arg1, arg2, ip);\n+}\n+#endif\n #endif /* _LINUX_KCOV_H */\ndiff --git a/include/linux/sched.h b/include/linux/sched.h\nindex eb12ff4cea6c2..589aa57e19124 100644\n--- a/include/linux/sched.h\n+++ b/include/linux/sched.h\n@@ -1553,6 +1553,40 @@ struct task_struct {\n \t/* KCOV sequence number: */\n \tint\t\t\t\tkcov_sequence;\n \n+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)\n+\t/*\n+\t * KCOV dataflow per-task record sequence counter (24 bits used) plus,\n+\t * in bit 31, the recursion guard held while a callback is running:\n+\t */\n+\tu32\t\t\t\tkcov_df_seq;\n+\n+\t/* KCOV dataflow: separate buffer for trace-args/trace-ret */\n+\tunsigned int\t\t\tkcov_df_size;\n+\tvoid\t\t\t\t*kcov_df_area;\n+\tbool\t\t\t\tkcov_df_enabled;\n+\n+\t/*\n+\t * The kcov_dataflow object this task's session belongs to, NULL when\n+\t * no session is active. The task holds a reference on it for the whole\n+\t * session, whether local (KCOV_DF_ENABLE, mirrors t-\u003ekcov) or remote\n+\t * (kcov_df_remote_start()), so the buffer can never be freed under an\n+\t * instrumented callback and both task exit and kcov_df_remote_stop()\n+\t * reach the exact object without a hash lookup.\n+\t */\n+\tstruct kcov_dataflow\t\t*kcov_df;\n+\n+\t/*\n+\t * Nesting depth of kcov_df_remote_start() on this task: 0 while no\n+\t * remote session is active (including during a local session), 1 for\n+\t * a normal bracketed work item. If a buggy caller nests, the inner\n+\t * start()s only bump this and the inner stop()s only decrement it, so\n+\t * the OUTER session (buffer + ref) is torn down exactly once, at the\n+\t * outermost stop -- never early, which would otherwise drop the ref\n+\t * and free the buffer out from under the still-running outer worker.\n+\t */\n+\tint\t\t\t\tkcov_df_remote_depth;\n+#endif\n+\n \t/* Collect coverage from softirq context: */\n \tunsigned int\t\t\tkcov_softirq;\n \ndiff --git a/include/uapi/linux/kcov_dataflow.h b/include/uapi/linux/kcov_dataflow.h\nnew file mode 100644\nindex 0000000000000..db3112a45832c\n--- /dev/null\n+++ b/include/uapi/linux/kcov_dataflow.h\n@@ -0,0 +1,92 @@\n+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */\n+#ifndef _LINUX_KCOV_DATAFLOW_H\n+#define _LINUX_KCOV_DATAFLOW_H\n+\n+#include \u003clinux/types.h\u003e\n+#include \u003clinux/ioctl.h\u003e\n+\n+/*\n+ * User space ABI of /sys/kernel/debug/kcov_dataflow, see\n+ * Documentation/dev-tools/kcov-dataflow.rst.\n+ *\n+ * KCOV_DF_INIT_TRACK takes the buffer size in u64 words by value (same\n+ * convention as KCOV_INIT_TRACE). KCOV_DF_REMOTE_ENABLE takes a pointer to a\n+ * __u64 remote handle encoded with kcov_remote_handle() (linux/kcov.h), so the\n+ * full 64-bit value survives 32-bit and compat callers.\n+ */\n+#define KCOV_DF_INIT_TRACK\t_IOR('d', 1, unsigned long)\n+#define KCOV_DF_ENABLE\t\t_IO('d', 100)\n+#define KCOV_DF_DISABLE\t\t_IO('d', 101)\n+#define KCOV_DF_REMOTE_ENABLE\t_IOW('d', 102, __u64)\n+#define KCOV_DF_REMOTE_DISABLE\t_IO('d', 103)\n+\n+/*\n+ * Buffer layout (all u64 words):\n+ *\n+ *   area[0]                number of record words written after area[0]\n+ *   area[1 + n ..]         records, back to back, each:\n+ *\n+ *     [0] header           see KCOV_DF_HDR_* below\n+ *     [1] pc               instrumented location; KASLR offset removed, like\n+ *                          the PCs mainline kcov records\n+ *     [2] ENTRY/RET: the traced value's address (full pointer); may be a\n+ *                    NULL/ERR_PTR value the callee received, in which case the\n+ *                    value words hold KCOV_DF_MAGIC_BAD\n+ *         CMP:       comparison type, KCOV_CMP_SIZE()/KCOV_CMP_CONST bits\n+ *                    (linux/kcov.h)\n+ *     [3 .. 3 + nvals)     value words: the scalar (nvals == 1), the expanded\n+ *                          struct fields, or the two CMP operands (nvals == 2)\n+ *\n+ * The header packs:\n+ *\n+ *   bits  0..23  per-task record sequence number\n+ *   bits 28..31  record type, KCOV_DF_TYPE_*\n+ *   bits 32..47  nvals, the number of value words that follow word [2]\n+ *   bits 48..55  ENTRY/RET: size in bytes of the traced argument/return value\n+ *                (clamped to 255)\n+ *   bits 56..63  ENTRY: argument index (clamped to 255); RET: 0\n+ *\n+ * A consumer walks the buffer as\n+ *\n+ *\tpos = 1;\n+ *\twhile (pos \u003c 1 + area[0]) {\n+ *\t\thdr = area[pos];\n+ *\t\tnvals = KCOV_DF_HDR_NVALS(hdr);\n+ *\t\t...\n+ *\t\tpos += KCOV_DF_RECORD_WORDS(nvals);\n+ *\t}\n+ *\n+ * area[0] never exceeds the buffer size minus one, and every counted word has\n+ * been written, so the walk above stays inside the mapping.\n+ */\n+#define KCOV_DF_TYPE_CMP\t0xC\n+#define KCOV_DF_TYPE_ENTRY\t0xE\n+#define KCOV_DF_TYPE_RET\t0xF\n+\n+#define KCOV_DF_HDR_SEQ_MASK\t0x00FFFFFFULL\n+#define KCOV_DF_HDR_TYPE_SHIFT\t28\n+#define KCOV_DF_HDR_TYPE_MASK\t0xFULL\n+#define KCOV_DF_HDR_NVALS_SHIFT\t32\n+#define KCOV_DF_HDR_NVALS_MASK\t0xFFFFULL\n+#define KCOV_DF_HDR_SIZE_SHIFT\t48\n+#define KCOV_DF_HDR_SIZE_MASK\t0xFFULL\n+#define KCOV_DF_HDR_ARGIDX_SHIFT 56\n+#define KCOV_DF_HDR_ARGIDX_MASK\t0xFFULL\n+\n+#define KCOV_DF_HDR_SEQ(h)\t((h) \u0026 KCOV_DF_HDR_SEQ_MASK)\n+#define KCOV_DF_HDR_TYPE(h)\t(((h) \u003e\u003e KCOV_DF_HDR_TYPE_SHIFT) \u0026 KCOV_DF_HDR_TYPE_MASK)\n+#define KCOV_DF_HDR_NVALS(h)\t(((h) \u003e\u003e KCOV_DF_HDR_NVALS_SHIFT) \u0026 KCOV_DF_HDR_NVALS_MASK)\n+#define KCOV_DF_HDR_SIZE(h)\t(((h) \u003e\u003e KCOV_DF_HDR_SIZE_SHIFT) \u0026 KCOV_DF_HDR_SIZE_MASK)\n+#define KCOV_DF_HDR_ARGIDX(h)\t(((h) \u003e\u003e KCOV_DF_HDR_ARGIDX_SHIFT) \u0026 KCOV_DF_HDR_ARGIDX_MASK)\n+\n+/* Words per record: header, pc, pointer/cmp-type, then the value words. */\n+#define KCOV_DF_RECORD_HDR_WORDS\t3\n+#define KCOV_DF_RECORD_WORDS(nvals)\t(KCOV_DF_RECORD_HDR_WORDS + (nvals))\n+\n+/* Largest nvals a record can carry; longer field lists are truncated. */\n+#define KCOV_DF_MAX_VALS\tKCOV_DF_HDR_NVALS_MASK\n+\n+/* Value word written when the traced pointer or a field could not be read. */\n+#define KCOV_DF_MAGIC_BAD\t0xBADADD85ULL\n+\n+#endif /* _LINUX_KCOV_DATAFLOW_H */\ndiff --git a/kernel/Makefile b/kernel/Makefile\nindex 1e1a31673577d..307b7fd1e1f96 100644\n--- a/kernel/Makefile\n+++ b/kernel/Makefile\n@@ -44,6 +44,12 @@ KCSAN_SANITIZE_kcov.o := n\n UBSAN_SANITIZE_kcov.o := n\n KMSAN_SANITIZE_kcov.o := n\n \n+KCOV_INSTRUMENT_kcov_dataflow.o := n\n+KASAN_SANITIZE_kcov_dataflow.o := n\n+KCSAN_SANITIZE_kcov_dataflow.o := n\n+UBSAN_SANITIZE_kcov_dataflow.o := n\n+KMSAN_SANITIZE_kcov_dataflow.o := n\n+\n CONTEXT_ANALYSIS_kcov.o := y\n CFLAGS_kcov.o := $(call cc-option, -fno-conserve-stack) -fno-stack-protector\n \n@@ -98,6 +104,9 @@ obj-$(CONFIG_AUDIT) += audit.o auditfilter.o\n obj-$(CONFIG_AUDITSYSCALL) += auditsc.o audit_watch.o audit_fsnotify.o audit_tree.o\n obj-$(CONFIG_GCOV_KERNEL) += gcov/\n obj-$(CONFIG_KCOV) += kcov.o\n+ifneq ($(CONFIG_KCOV_DATAFLOW_ARGS)$(CONFIG_KCOV_DATAFLOW_RET),)\n+obj-y += kcov_dataflow.o\n+endif\n obj-$(CONFIG_KPROBES) += kprobes.o\n obj-$(CONFIG_FAIL_FUNCTION) += fail_function.o\n obj-$(CONFIG_KGDB) += debug/\ndiff --git a/kernel/exit.c b/kernel/exit.c\nindex 97686af895013..8881661d635ba 100644\n--- a/kernel/exit.c\n+++ b/kernel/exit.c\n@@ -939,6 +939,7 @@ void __noreturn do_exit(long code)\n \t\tkthread_do_exit(kthread, code);\n \n \tkcov_task_exit(tsk);\n+\tkcov_dataflow_task_exit(tsk);\n \tkmsan_task_exit(tsk);\n \n \tsynchronize_group_exit(tsk, code);\ndiff --git a/kernel/fork.c b/kernel/fork.c\nindex 22283bf849e15..14d4fe5c7909b 100644\n--- a/kernel/fork.c\n+++ b/kernel/fork.c\n@@ -985,6 +985,7 @@ static struct task_struct *dup_task_struct(struct task_struct *orig, int node)\n \ttsk-\u003eworker_private = NULL;\n \n \tkcov_task_init(tsk);\n+\tkcov_dataflow_task_init(tsk);\n \tkmsan_task_create(tsk);\n \tkmap_local_fork(tsk);\n \ndiff --git a/kernel/kcov.c b/kernel/kcov.c\nindex 35420f0ac524d..cac9b69e197ed 100644\n--- a/kernel/kcov.c\n+++ b/kernel/kcov.c\n@@ -232,7 +232,14 @@ void notrace __sanitizer_cov_trace_pc(void)\n EXPORT_SYMBOL(__sanitizer_cov_trace_pc);\n \n #ifdef CONFIG_KCOV_ENABLE_COMPARISONS\n-static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)\n+/*\n+ * Mainline kcov comparison writer: appends to the task's own kcov buffer, and\n+ * only in KCOV_MODE_TRACE_CMP. The fan-out that also feeds the kcov-dataflow\n+ * buffer lives in kcov_trace_cmp() in \u003clinux/kcov.h\u003e, so kcov.c never references\n+ * the dataflow side itself. This writer is only non-static so that header helper\n+ * (which the cmp callbacks below call) can reach it.\n+ */\n+void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)\n {\n \tstruct task_struct *t;\n \tu64 *area;\n@@ -267,55 +274,59 @@ static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)\n \t}\n }\n \n+/*\n+ * The __sanitizer_cov_trace_cmp*() callbacks stay here in kcov.c (one shared,\n+ * compiler-emitted symbol per comparison -- no separate df_cmp symbol, no\n+ * compiler change). Each routes its operand pair through kcov_trace_cmp()\n+ * (defined in \u003clinux/kcov.h\u003e), which records into mainline kcov and, when this\n+ * task has a dataflow session, into kcov-dataflow too. kcov.c never names the\n+ * dataflow side; that fan-out lives entirely in the header.\n+ */\n void notrace __sanitizer_cov_trace_cmp1(u8 arg1, u8 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_cmp1);\n \n void notrace __sanitizer_cov_trace_cmp2(u16 arg1, u16 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_cmp2);\n \n void notrace __sanitizer_cov_trace_cmp4(u32 arg1, u32 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_cmp4);\n \n void notrace __sanitizer_cov_trace_cmp8(kcov_u64 arg1, kcov_u64 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_cmp8);\n \n void notrace __sanitizer_cov_trace_const_cmp1(u8 arg1, u8 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2,\n-\t\t\t_RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp1);\n \n void notrace __sanitizer_cov_trace_const_cmp2(u16 arg1, u16 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2,\n-\t\t\t_RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp2);\n \n void notrace __sanitizer_cov_trace_const_cmp4(u32 arg1, u32 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2,\n-\t\t\t_RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp4);\n \n void notrace __sanitizer_cov_trace_const_cmp8(kcov_u64 arg1, kcov_u64 arg2)\n {\n-\twrite_comp_data(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2,\n-\t\t\t_RET_IP_);\n+\tkcov_trace_cmp(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp8);\n \n@@ -344,7 +355,7 @@ void notrace __sanitizer_cov_trace_switch(kcov_u64 val, void *arg)\n \t\treturn;\n \t}\n \tfor (i = 0; i \u003c count; i++)\n-\t\twrite_comp_data(type, cases[i + 2], val, _RET_IP_);\n+\t\tkcov_trace_cmp(type, cases[i + 2], val, _RET_IP_);\n }\n EXPORT_SYMBOL(__sanitizer_cov_trace_switch);\n #endif /* ifdef CONFIG_KCOV_ENABLE_COMPARISONS */\n@@ -587,23 +598,6 @@ static void kcov_fault_in_area(struct kcov *kcov)\n \t\tREAD_ONCE(area[offset]);\n }\n \n-static inline bool kcov_check_handle(u64 handle, bool common_valid,\n-\t\t\t\tbool uncommon_valid, bool zero_valid)\n-{\n-\tif (handle \u0026 ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))\n-\t\treturn false;\n-\tswitch (handle \u0026 KCOV_SUBSYSTEM_MASK) {\n-\tcase KCOV_SUBSYSTEM_COMMON:\n-\t\treturn (handle \u0026 KCOV_INSTANCE_MASK) ?\n-\t\t\tcommon_valid : zero_valid;\n-\tcase KCOV_SUBSYSTEM_USB:\n-\t\treturn uncommon_valid;\n-\tdefault:\n-\t\treturn false;\n-\t}\n-\treturn false;\n-}\n-\n static int kcov_ioctl_locked(struct kcov *kcov, unsigned int cmd,\n \t\t\t     unsigned long arg)\n \t__must_hold(\u0026kcov-\u003elock)\ndiff --git a/kernel/kcov_dataflow.c b/kernel/kcov_dataflow.c\nnew file mode 100644\nindex 0000000000000..641d6bc763864\n--- /dev/null\n+++ b/kernel/kcov_dataflow.c\n@@ -0,0 +1,1193 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * KCOV Dataflow: per-task function argument/return value capture.\n+ *\n+ * Exposes /sys/kernel/debug/kcov_dataflow, completely independent from\n+ * /sys/kernel/debug/kcov. Own buffer, own ioctl, own mmap.\n+ *\n+ * The user-visible ABI:\n+ *\n+ * ioctls, the record layout and the header bit fields, is defined in\n+ * \u003cuapi/linux/kcov_dataflow.h\u003e. In short, every record is\n+ *\n+ *   [hdr][pc][ptr or cmp type][nvals value words]\n+ *\n+ * appended after area[0], which counts the record words written so far.\n+ */\n+#define pr_fmt(fmt) \"kcov_dataflow: \" fmt\n+\n+#define DISABLE_BRANCH_PROFILING\n+#include \u003clinux/atomic.h\u003e\n+#include \u003clinux/bits.h\u003e\n+#include \u003clinux/compiler.h\u003e\n+#include \u003clinux/errno.h\u003e\n+#include \u003clinux/export.h\u003e\n+#include \u003clinux/types.h\u003e\n+#include \u003clinux/file.h\u003e\n+#include \u003clinux/fs.h\u003e\n+#include \u003clinux/init.h\u003e\n+#include \u003clinux/minmax.h\u003e\n+#include \u003clinux/mm.h\u003e\n+#include \u003clinux/preempt.h\u003e\n+#include \u003clinux/refcount.h\u003e\n+#include \u003clinux/sched.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/shrinker.h\u003e\n+#include \u003clinux/mutex.h\u003e\n+#include \u003clinux/hashtable.h\u003e\n+#include \u003clinux/vmalloc.h\u003e\n+#include \u003clinux/debugfs.h\u003e\n+#include \u003clinux/uaccess.h\u003e\n+#include \u003clinux/jump_label.h\u003e\n+#include \u003clinux/kcov.h\u003e\n+#include \u003cuapi/linux/kcov_dataflow.h\u003e\n+#include \u003casm/setup.h\u003e\n+\n+/*\n+ * Comparison capture is shared with mainline kcov; it only exists when both the\n+ * trace-cmp instrumentation and the dataflow task state are configured in.\n+ */\n+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) \u0026\u0026 \\\n+\t(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))\n+#define KCOV_DF_HAVE_CMP 1\n+#endif\n+\n+#define KCOV_DF_IS_ERR(p)\t((unsigned long)(p) \u003e= (unsigned long)-4095UL)\n+\n+/*\n+ * Bit 31 of task_struct::kcov_df_seq is the per-task recursion guard, held\n+ * while one of the callbacks below runs. The record sequence number lives in\n+ * the low 24 bits (KCOV_DF_HDR_SEQ_MASK) and is advanced with kcov_df_next_seq()\n+ * so that it wraps inside its own field and can never carry into the guard.\n+ */\n+#define KCOV_DF_SEQ_GUARD\tBIT(31)\n+\n+/*\n+ * Per-worker private scratch size (u64 words), KCOV's remote-area model: a\n+ * remote kworker collects into its OWN scratch and merges it into the shared\n+ * -\u003earea at kcov_df_remote_stop(). Fixed and small (8 MiB) -- one work item's\n+ * coverage, not a whole buffer -- so the pool of recycled scratch areas stays\n+ * bounded regardless of how many kworkers churn. Overflowing a scratch just\n+ * drops that worker's excess records (same as a full buffer), never corrupts.\n+ */\n+#define KCOV_DF_REMOTE_WORDS\t(1UL \u003c\u003c 20)\n+\n+struct kcov_dataflow {\n+\tstruct mutex\tlock;\n+\tunsigned int\tsize;\t/* in u64 words */\n+\tvoid\t\t*area;\n+\t/*\n+\t * Task with a local (KCOV_DF_ENABLE) session on this object, NULL if\n+\t * none. Mirrors struct kcov::t: that task holds its own reference (see\n+\t * -\u003erefcount) and points back at us through task_struct::kcov_df, so\n+\t * KCOV_DF_DISABLE, close() and task exit all unwire the same session.\n+\t */\n+\tstruct task_struct *t;\n+\t/*\n+\t * Lifetime refcount (KCOV's struct kcov pattern). The open fd holds one\n+\t * ref; the task enabled with KCOV_DF_ENABLE holds one for as long as its\n+\t * session lasts (dropped by KCOV_DF_DISABLE, by close() from that task,\n+\t * or by task exit -- it cannot be unwired from another task); each\n+\t * kcov_df_remote_start() takes one and the matching kcov_df_remote_stop()\n+\t * drops it. Whoever drops the LAST ref frees -\u003earea and the object\n+\t * (kcov_df_put), so an instrumented callback can never write through a\n+\t * freed buffer, whichever task does the final close().\n+\t */\n+\trefcount_t\trefcount;\n+\tu64\t\tremote_handle; /* handle for remote lookup, 0 if not published */\n+#ifdef KCOV_DF_HAVE_CMP\n+\t/*\n+\t * Whether this fd holds a ref on kcov_df_cmp_key, tracked SEPARATELY for\n+\t * the local (KCOV_DF_ENABLE) and remote (KCOV_DF_REMOTE_ENABLE) sources.\n+\t * A single shared flag let a KCOV_DF_DISABLE drop the key while a remote\n+\t * handle was still published -- silently losing the live remote workers'\n+\t * comparison records. Two flags mean releasing one source never pulls the\n+\t * key out from under the other. Both are only touched under -\u003elock.\n+\t */\n+\tbool\t\tcmp_key_local;\n+\tbool\t\tcmp_key_remote;\n+#endif\n+};\n+\n+/* Which activation source holds the cmp static key (see kcov_df_cmp_key_hold). */\n+enum { KCOV_DF_CMP_LOCAL, KCOV_DF_CMP_REMOTE };\n+\n+#ifdef KCOV_DF_HAVE_CMP\n+/*\n+ * Static key gating the per-comparison dataflow check in kcov_trace_cmp()\n+ * (linux/kcov.h). It is a patched-out NOP until at least one dataflow session is\n+ * live, so trace-cmp across the WHOLE kernel costs nothing extra while no\n+ * dataflow fuzzing runs; only an active session flips it on. Refcounted: inc on\n+ * each source's first enable, dec on its disable/close/exit (idempotent,\n+ * tracked per source via cmp_key_local / cmp_key_remote so releasing one never\n+ * drops the key from under the other).\n+ *\n+ * The key is only ever inc'd/dec'd from ioctl, close() and do_exit() context,\n+ * under df-\u003elock -- never from kcov_df_remote_stop() or the last kcov_df_put(),\n+ * so a subsystem's worker path never ends up under cpus_read_lock() and\n+ * jump_label_mutex. The static_branch_{inc,dec}() text-patch is amortised -- it\n+ * fires only on the 0-\u003e1 and 1-\u003e0 transitions, not per fd while sessions overlap.\n+ */\n+DEFINE_STATIC_KEY_FALSE(kcov_df_cmp_key);\n+EXPORT_SYMBOL(kcov_df_cmp_key);\n+\n+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which)\n+{\n+\tbool *held = which == KCOV_DF_CMP_LOCAL ? \u0026df-\u003ecmp_key_local\n+\t\t\t\t\t\t: \u0026df-\u003ecmp_key_remote;\n+\n+\tlockdep_assert_held(\u0026df-\u003elock);\n+\tif (!*held) {\n+\t\t*held = true;\n+\t\tstatic_branch_inc(\u0026kcov_df_cmp_key);\n+\t}\n+}\n+\n+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which)\n+{\n+\tbool *held = which == KCOV_DF_CMP_LOCAL ? \u0026df-\u003ecmp_key_local\n+\t\t\t\t\t\t: \u0026df-\u003ecmp_key_remote;\n+\n+\tlockdep_assert_held(\u0026df-\u003elock);\n+\tif (*held) {\n+\t\t*held = false;\n+\t\tstatic_branch_dec(\u0026kcov_df_cmp_key);\n+\t}\n+}\n+\n+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df)\n+{\n+\treturn df-\u003ecmp_key_local || df-\u003ecmp_key_remote;\n+}\n+#else\n+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which) {}\n+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which) {}\n+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df) { return false; }\n+#endif\n+\n+/* Remote dataflow: handle-based lookup (follows KCOV's kcov_remote_map pattern) */\n+static DEFINE_MUTEX(kcov_df_remote_lock);\n+static DEFINE_HASHTABLE(kcov_df_remote_map, 4);\n+\n+struct kcov_df_remote {\n+\tu64\t\t\thandle;\n+\tstruct kcov_dataflow\t*df;\n+\tstruct hlist_node\thnode;\n+};\n+\n+static struct kcov_df_remote *kcov_df_remote_find(u64 handle)\n+{\n+\tstruct kcov_df_remote *remote;\n+\n+\thash_for_each_possible(kcov_df_remote_map, remote, hnode, handle) {\n+\t\tif (remote-\u003ehandle == handle)\n+\t\t\treturn remote;\n+\t}\n+\treturn NULL;\n+}\n+\n+/* Unpublish @df's remote handle, if any; no new remote session can start. */\n+static void kcov_df_remote_unpublish(struct kcov_dataflow *df)\n+{\n+\tstruct kcov_df_remote *remote;\n+\n+\tmutex_lock(\u0026kcov_df_remote_lock);\n+\tif (df-\u003eremote_handle) {\n+\t\tremote = kcov_df_remote_find(df-\u003eremote_handle);\n+\t\tif (remote) {\n+\t\t\thash_del(\u0026remote-\u003ehnode);\n+\t\t\tkfree(remote);\n+\t\t}\n+\t\tdf-\u003eremote_handle = 0;\n+\t}\n+\tmutex_unlock(\u0026kcov_df_remote_lock);\n+}\n+\n+static void kcov_df_get(struct kcov_dataflow *df)\n+{\n+\trefcount_inc(\u0026df-\u003erefcount);\n+}\n+\n+/*\n+ * Drop a reference; the last one frees the buffer and the object. Only called\n+ * from sleepable task context (ioctl, close(), do_exit(), and remote_stop()\n+ * which requires it), so vfree() here is fine. No caller may touch @df after\n+ * its own kcov_df_put(). Every path that unwires a session releases its cmp\n+ * key ref under df-\u003elock first, so nothing is left to balance here.\n+ */\n+static void kcov_df_put(struct kcov_dataflow *df)\n+{\n+\tif (refcount_dec_and_test(\u0026df-\u003erefcount)) {\n+\t\tWARN_ON_ONCE(kcov_df_cmp_key_held(df));\n+\t\tvfree(df-\u003earea);\n+\t\tkfree(df);\n+\t}\n+}\n+\n+/*\n+ * Touch every page of a buffer before a task starts collecting into it, the\n+ * same way kcov_fault_in_area() does for KCOV_ENABLE: on configurations with\n+ * lazily populated vmalloc mappings the first access would otherwise fault\n+ * from inside an instrumented callback, and code on the vmalloc fault path may\n+ * itself be instrumented.\n+ */\n+static void kcov_df_fault_in_area(u64 *area, unsigned long size)\n+{\n+\tunsigned long stride = PAGE_SIZE / sizeof(u64);\n+\tunsigned long off;\n+\n+\tfor (off = 0; off \u003c size; off += stride)\n+\t\tREAD_ONCE(area[off]);\n+}\n+\n+/*\n+ * Pool of recycled per-worker scratch areas (KCOV's kcov_remote_areas). All are\n+ * KCOV_DF_REMOTE_WORDS u64s. While parked on the freelist the area's first bytes\n+ * hold this list_head; while in use word[0] is the scratch write cursor. Guarded\n+ * by kcov_df_remote_lock.\n+ */\n+struct kcov_df_scratch {\n+\tstruct list_head list;\n+};\n+static LIST_HEAD(kcov_df_scratch_pool);\n+static unsigned long kcov_df_scratch_pool_nr;\t/* idle areas parked in the pool */\n+\n+/* Take a scratch area from the pool, or NULL if empty (caller vmalloc()s one). */\n+static void *kcov_df_scratch_get(void)\n+{\n+\tstruct kcov_df_scratch *s;\n+\n+\tif (list_empty(\u0026kcov_df_scratch_pool))\n+\t\treturn NULL;\n+\ts = list_first_entry(\u0026kcov_df_scratch_pool, struct kcov_df_scratch, list);\n+\tlist_del(\u0026s-\u003elist);\n+\tkcov_df_scratch_pool_nr--;\n+\treturn s;\n+}\n+\n+/* Return a scratch area to the pool for reuse. */\n+static void kcov_df_scratch_put(void *area)\n+{\n+\tstruct kcov_df_scratch *s = area;\n+\n+\tINIT_LIST_HEAD(\u0026s-\u003elist);\n+\tlist_add(\u0026s-\u003elist, \u0026kcov_df_scratch_pool);\n+\tkcov_df_scratch_pool_nr++;\n+}\n+\n+/*\n+ * Merge a remote worker's private scratch into the shared -\u003earea, appending its\n+ * records at the shared write cursor. This is the ONE many-writers path (several\n+ * kworkers merge concurrently), so it claims its region with a cmpxchg loop on\n+ * area[0]: the bounds are checked against the value about to be committed, and\n+ * the commit only happens when the record fits. area[0] therefore never exceeds\n+ * the buffer capacity and every counted word has been written, so a consumer\n+ * walking area[0] words stays inside its mapping. A concurrent reset by user\n+ * space (writing area[0] = 0 to restart collection) simply makes the cmpxchg\n+ * fail and the loop re-read the new cursor; there is no subtract, so the\n+ * counter can never go negative or wrap past the bounds check. Each merge claims\n+ * a disjoint [start, start+n), so concurrent merges don't overlap and need no\n+ * lock. @df is kept alive by the caller's reference, so -\u003earea is stable here.\n+ *\n+ * -\u003earea is never written through kcov_df_reserve() while a remote handle is\n+ * published (KCOV_DF_ENABLE refuses that), so this atomic cursor update never\n+ * races a plain read-modify-write of the same word.\n+ */\n+static void kcov_df_merge(struct kcov_dataflow *df, const u64 *scratch)\n+{\n+\tu64 *area = df-\u003earea;\n+\tatomic64_t *cursor;\n+\tu64 n, count, capacity;\n+\ts64 old;\n+\n+\tif (!area)\n+\t\treturn;\n+\t/*\n+\t * scratch[0] is an EXACT high-water of written words: kcov_df_reserve()\n+\t * commits the count only after a record fits, so every counted word was\n+\t * really written -- the merge never publishes the unwritten\n+\t * (recycled/uninitialized) tail of a pooled scratch. The clamp below is thus\n+\t * belt-and-suspenders against a stray count.\n+\t */\n+\tn = scratch[0];\n+\tif (n \u003e KCOV_DF_REMOTE_WORDS - 1)\n+\t\tn = KCOV_DF_REMOTE_WORDS - 1;\n+\tif (!n)\n+\t\treturn;\n+\n+\tcapacity = df-\u003esize - 1;\t/* words after area[0] */\n+\tcursor = (atomic64_t *)\u0026area[0];\n+\told = atomic64_read(cursor);\n+\tdo {\n+\t\tcount = old;\n+\t\t/* Full (or a garbage cursor from user space): drop the records. */\n+\t\tif (count \u003e capacity || n \u003e capacity - count)\n+\t\t\treturn;\n+\t} while (!atomic64_try_cmpxchg(cursor, \u0026old, count + n));\n+\tmemcpy(\u0026area[1 + count], \u0026scratch[1], n * sizeof(u64));\n+}\n+\n+/*\n+ * Reserve @record_len u64 words in the current task's buffer. On success return\n+ * true and store the 1-based start index of the record's data region.\n+ *\n+ * Single-writer discipline, identical to mainline kcov.c: the current task is the\n+ * ONLY instrumented writer of @area. In remote mode @area is this kworker's OWN\n+ * private scratch; in local (KCOV_DF_ENABLE) mode it is the enabling task's own\n+ * mmapped buffer -- and only one task can hold that (the KCOV_DF_ENABLE EBUSY\n+ * guard, which also refuses a buffer with a published remote handle, so\n+ * kcov_df_merge() never touches this word concurrently). Two tasks never write\n+ * the same @area here, so no atomic is needed: validate FIRST and commit the\n+ * count (area[0]) only on success, so area[0] is always an EXACT high-water of\n+ * written words and no consumer (userspace or kcov_df_merge()) ever sees an\n+ * unwritten/recycled slot.\n+ *\n+ * (Publishing a worker's scratch into the shared -\u003earea is the SEPARATE\n+ * kcov_df_merge() path, which DOES reserve atomically because many kworkers merge\n+ * concurrently.)\n+ *\n+ * READ_ONCE/WRITE_ONCE because in local mode userspace may reset area[0] to 0\n+ * between operations. That reset can only drive the count to 0, never negative\n+ * (there is no subtract), so a racing reset may drop records but can never produce\n+ * an out-of-bounds store. This is exactly mainline kcov's contract.\n+ *\n+ * __always_inline because kcov_df_trace_cmp() below is on objtool's\n+ * uaccess_safe_builtin[] list, and objtool rejects any out-of-line call made\n+ * from such a function; do not leave that to the optimizer.\n+ */\n+static __always_inline notrace __no_sanitize_coverage bool\n+kcov_df_reserve(struct task_struct *t, u64 *area, u32 record_len,\n+\t\tunsigned long *start_index)\n+{\n+\tunsigned long count = READ_ONCE(area[0]);\n+\n+\t*start_index = 1 + count;\n+\tif (count \u003e= t-\u003ekcov_df_size ||\n+\t    record_len \u003e t-\u003ekcov_df_size - *start_index)\n+\t\treturn false;\n+\tWRITE_ONCE(area[0], count + record_len);\n+\treturn true;\n+}\n+\n+/*\n+ * Contexts where dataflow collection must stay completely inert.\n+ *\n+ * Beyond the obvious !in_task() case, this bails whenever page faults are\n+ * disabled. copy_from_kernel_nofault() -- used by kcov_df_write() below to read\n+ * traced pointers, and, crucially, by the ORC stack unwinder that KASAN runs on\n+ * every slab free (set_track_prepare() -\u003e stack_trace_save()) -- brackets its\n+ * raw loads with pagefault_disable(), and those loads carry trace-cmp/trace-args\n+ * instrumentation. Without this bail a single stack walk under a fuzzing + KASAN\n+ * workload floods the collector with a callback per load and soft-locks the CPU.\n+ *\n+ * pagefault_disabled() is true throughout any such nofault region no matter\n+ * which instrumented leaf issued the callback, so testing it here contains the\n+ * whole class of self-instrumentation storms -- the bit-31 recursion guard below\n+ * only covers re-entry nested inside our own callback, not a fresh entry from\n+ * the unwinder/KASAN path. Contained entirely to this file: no coverage\n+ * exclusion in mm/ or arch/ is needed.\n+ *\n+ * The trade-off is that records are also dropped inside unrelated\n+ * pagefault_disable() regions (kmap_atomic() on HIGHMEM, futex and perf\n+ * callchain probes, ...). Those are short and rare on the fuzzing workloads this\n+ * targets; a per-task \"in nofault region\" flag would remove the coupling at the\n+ * cost of touching mm/maccess.c.\n+ */\n+static __always_inline notrace __no_sanitize_coverage bool\n+kcov_df_inert_context(void)\n+{\n+\treturn !in_task() || pagefault_disabled();\n+}\n+\n+/* Same as kcov.c: record PCs with the KASLR offset removed. */\n+static __always_inline notrace __no_sanitize_coverage u64\n+kcov_df_canonicalize_ip(u64 ip)\n+{\n+#ifdef CONFIG_RANDOMIZE_BASE\n+\tip -= kaslr_offset();\n+#endif\n+\treturn ip;\n+}\n+\n+/*\n+ * Advance the task's 24-bit record sequence number, keeping the guard bit set.\n+ * Masking the increment keeps the counter from ever carrying into\n+ * KCOV_DF_SEQ_GUARD, which would reopen re-entry in the middle of a record.\n+ */\n+static __always_inline notrace __no_sanitize_coverage u32\n+kcov_df_next_seq(struct task_struct *t)\n+{\n+\tu32 seq = (t-\u003ekcov_df_seq + 1) \u0026 KCOV_DF_HDR_SEQ_MASK;\n+\n+\tt-\u003ekcov_df_seq = KCOV_DF_SEQ_GUARD | seq;\n+\treturn seq;\n+}\n+\n+static __always_inline notrace __no_sanitize_coverage u64\n+kcov_df_hdr(u64 type, u32 nvals, u32 size, u32 arg_idx, u32 seq)\n+{\n+\treturn (type \u003c\u003c KCOV_DF_HDR_TYPE_SHIFT) |\n+\t       ((u64)nvals \u003c\u003c KCOV_DF_HDR_NVALS_SHIFT) |\n+\t       ((u64)min_t(u32, size, KCOV_DF_HDR_SIZE_MASK) \u003c\u003c\n+\t\tKCOV_DF_HDR_SIZE_SHIFT) |\n+\t       ((u64)min_t(u32, arg_idx, KCOV_DF_HDR_ARGIDX_MASK) \u003c\u003c\n+\t\tKCOV_DF_HDR_ARGIDX_SHIFT) |\n+\t       (seq \u0026 KCOV_DF_HDR_SEQ_MASK);\n+}\n+\n+/*\n+ * Core write function for ENTRY/RET records.\n+ * Uses the same READ_ONCE/WRITE_ONCE pattern as write_comp_data() in kcov.c.\n+ *\n+ * @num_fields is the length of the compiler-supplied @offsets table (pairs of\n+ * offset,size) for an expanded struct, 0 for a scalar read directly from @ptr\n+ * with width @size. It is clamped to KCOV_DF_MAX_VALS so the record length can\n+ * never wrap and the field loop is bounded by the words actually reserved.\n+ */\n+static noinline notrace __no_sanitize_coverage void\n+kcov_df_write(u64 type, u64 pc, u32 arg_idx, u32 size, void *ptr,\n+\t      u64 *offsets, u32 num_fields)\n+{\n+\tstruct task_struct *t = current;\n+\tu64 *area;\n+\tunsigned long start_index;\n+\tu32 nvals, seq, i;\n+\n+\tif (kcov_df_inert_context())\n+\t\treturn;\n+\n+\tif (!t-\u003ekcov_df_enabled)\n+\t\treturn;\n+\n+\t/*\n+\t * Prevent recursion: functions called by this callback\n+\t * (copy_from_kernel_nofault) may be instrumented. Use the\n+\t * sequence counter's high bit as a per-task guard.\n+\t */\n+\tif (t-\u003ekcov_df_seq \u0026 KCOV_DF_SEQ_GUARD)\n+\t\treturn;\n+\tt-\u003ekcov_df_seq |= KCOV_DF_SEQ_GUARD;\n+\t/* Paired with the barrier() before the guard is cleared at out:. */\n+\tbarrier();\n+\n+\tarea = (u64 *)t-\u003ekcov_df_area;\n+\tif (!area)\n+\t\tgoto out;\n+\n+\tif (num_fields \u003e KCOV_DF_MAX_VALS)\n+\t\tnum_fields = KCOV_DF_MAX_VALS;\n+\t/* Record: header + pc + ptr, then the fields or one scalar word. */\n+\tnvals = num_fields \u003e 0 ? num_fields : 1;\n+\n+\tif (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(nvals), \u0026start_index))\n+\t\tgoto out;\n+\n+\tseq = kcov_df_next_seq(t);\n+\tarea[start_index] = kcov_df_hdr(type, nvals, size, arg_idx, seq);\n+\tarea[start_index + 1] = kcov_df_canonicalize_ip(pc);\n+\tarea[start_index + 2] = (u64)(unsigned long)ptr;\n+\n+\tif (num_fields == 0) {\n+\t\tu64 val = 0;\n+\t\tu32 sz = size;\n+\n+\t\t/*\n+\t\t * Read the scalar with a compile-time-constant width for the\n+\t\t * common sizes so the compiler folds away copy_from_kernel_\n+\t\t * nofault()'s runtime size loop and alignment branching; fall\n+\t\t * back to the variable-size byte copy for anything else. A\n+\t\t * faulting read leaves val == 0, matching the prior best-effort\n+\t\t * behaviour.\n+\t\t */\n+\t\tif (ptr \u0026\u0026 !KCOV_DF_IS_ERR(ptr)) {\n+\t\t\tswitch (sz) {\n+\t\t\tcase 8: {\n+\t\t\t\tu64 v = 0;\n+\n+\t\t\t\tif (!get_kernel_nofault(v, (u64 *)ptr))\n+\t\t\t\t\tval = v;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tcase 4: {\n+\t\t\t\tu32 v = 0;\n+\n+\t\t\t\tif (!get_kernel_nofault(v, (u32 *)ptr))\n+\t\t\t\t\tval = v;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tcase 2: {\n+\t\t\t\tu16 v = 0;\n+\n+\t\t\t\tif (!get_kernel_nofault(v, (u16 *)ptr))\n+\t\t\t\t\tval = v;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tcase 1: {\n+\t\t\t\tu8 v = 0;\n+\n+\t\t\t\tif (!get_kernel_nofault(v, (u8 *)ptr))\n+\t\t\t\t\tval = v;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tdefault:\n+\t\t\t\tif (sz \u003e sizeof(val))\n+\t\t\t\t\tsz = sizeof(val);\n+\t\t\t\tcopy_from_kernel_nofault(\u0026val, ptr, sz);\n+\t\t\t}\n+\t\t}\n+\t\tarea[start_index + 3] = val;\n+\t} else {\n+\t\tif (!ptr || KCOV_DF_IS_ERR(ptr)) {\n+\t\t\tfor (i = 0; i \u003c num_fields; i++)\n+\t\t\t\tarea[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;\n+\t\t\tgoto out;\n+\t\t}\n+\t\tfor (i = 0; i \u003c num_fields; i++) {\n+\t\t\tu64 off, sz, val = KCOV_DF_MAGIC_BAD;\n+\t\t\tvoid *fa;\n+\n+\t\t\tif (copy_from_kernel_nofault(\u0026off, \u0026offsets[i * 2], sizeof(off)) ||\n+\t\t\t    copy_from_kernel_nofault(\u0026sz, \u0026offsets[i * 2 + 1], sizeof(sz))) {\n+\t\t\t\tarea[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tfa = (void *)((unsigned long)ptr + off);\n+\t\t\tval = 0;\n+\n+\t\t\tif (sz \u003c= sizeof(val)) {\n+\t\t\t\tif (copy_from_kernel_nofault(\u0026val, fa, sz))\n+\t\t\t\t\tval = KCOV_DF_MAGIC_BAD;\n+\t\t\t} else {\n+\t\t\t\tif (copy_from_kernel_nofault(\u0026val, fa, sizeof(val)))\n+\t\t\t\t\tval = KCOV_DF_MAGIC_BAD;\n+\t\t\t}\n+\t\t\tarea[start_index + 3 + i] = val;\n+\t\t}\n+\t}\n+out:\n+\t/*\n+\t * Paired with the barrier() after setting the guard at the top.\n+\t * Ensures all record writes are complete before we clear the\n+\t * recursion guard.\n+\t */\n+\tbarrier();\n+\tt-\u003ekcov_df_seq \u0026= ~KCOV_DF_SEQ_GUARD;\n+}\n+\n+/*\n+ * The two compiler-emitted entry points are on objtool's uaccess_safe_builtin[]\n+ * list, like the __sanitizer_cov_trace_cmp*() callbacks. The trace-args call is\n+ * planted before the terminator of the function's entry block (so that every\n+ * spilled value dominates it), not at its first instruction: a function that\n+ * opens a user access region and then does an unsafe_get_user() -- an asm goto,\n+ * hence a block terminator -- gets the callback AFTER the stac, and objtool\n+ * reports \"call to __sanitizer_cov_trace_args() with UACCESS enabled\".\n+ *\n+ * objtool validates a listed function with AC set and rejects any out-of-line\n+ * call from it, and kcov_df_write() calls copy_from_kernel_nofault(), so bracket\n+ * the call with user_access_save()/restore(): that clears AC for the whole\n+ * record write (the kasan_report() pattern) and keeps SMAP/PAN protection in\n+ * force while the collector runs. It compiles to nothing on architectures\n+ * without the feature.\n+ */\n+#ifdef CONFIG_KCOV_DATAFLOW_ARGS\n+noinline void notrace __no_sanitize_coverage\n+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,\n+\t\t\t   u64 *offsets, u32 num_fields);\n+\n+noinline void notrace __no_sanitize_coverage\n+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,\n+\t\t\t   u64 *offsets, u32 num_fields)\n+{\n+\tunsigned long ua_flags = user_access_save();\n+\n+\tkcov_df_write(KCOV_DF_TYPE_ENTRY, pc, arg_idx, arg_size, arg_ptr,\n+\t\t      offsets, num_fields);\n+\tuser_access_restore(ua_flags);\n+}\n+EXPORT_SYMBOL(__sanitizer_cov_trace_args);\n+#endif\n+\n+#ifdef CONFIG_KCOV_DATAFLOW_RET\n+noinline void notrace __no_sanitize_coverage\n+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,\n+\t\t\t  u64 *offsets, u32 num_fields);\n+\n+noinline void notrace __no_sanitize_coverage\n+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,\n+\t\t\t  u64 *offsets, u32 num_fields)\n+{\n+\tunsigned long ua_flags = user_access_save();\n+\n+\tkcov_df_write(KCOV_DF_TYPE_RET, pc, 0, ret_size, ret_val,\n+\t\t      offsets, num_fields);\n+\tuser_access_restore(ua_flags);\n+}\n+EXPORT_SYMBOL(__sanitizer_cov_trace_ret);\n+#endif\n+\n+#ifdef KCOV_DF_HAVE_CMP\n+/*\n+ * Comparison capture (input-to-state). Reached from the shared\n+ * __sanitizer_cov_trace_cmp*() callbacks (kcov.c) via kcov_trace_cmp()\n+ * (linux/kcov.h), which fans out to mainline kcov and, when this task has a\n+ * dataflow session, here as well, so trace-cmp operand pairs land in the SAME\n+ * unified TLV buffer as the arg/ret records. Both operands are recorded, so a\n+ * userspace consumer can use them for input-to-state matching, complementing\n+ * the arg/ret records.\n+ *\n+ * Record: [header(CMP|nvals=2|seq)][pc][cmp_type][arg1][arg2].\n+ * cmp_type carries KCOV_CMP_SIZE()/KCOV_CMP_CONST bits (see linux/kcov.h) so the\n+ * consumer knows operand width and whether one side was a compile-time constant.\n+ *\n+ * On objtool's uaccess_safe_builtin[] list, so this function makes no\n+ * out-of-line call (kcov_df_reserve() and the helpers are __always_inline).\n+ */\n+noinline notrace __no_sanitize_coverage void\n+kcov_df_trace_cmp(u64 cmp_type, u64 arg1, u64 arg2, u64 ip)\n+{\n+\tstruct task_struct *t = current;\n+\tu64 *area;\n+\tunsigned long start_index;\n+\tu32 seq;\n+\n+\tif (kcov_df_inert_context())\n+\t\treturn;\n+\tif (!t-\u003ekcov_df_enabled)\n+\t\treturn;\n+\t/* Same recursion guard as kcov_df_write(): bit 31 of the seq counter. */\n+\tif (t-\u003ekcov_df_seq \u0026 KCOV_DF_SEQ_GUARD)\n+\t\treturn;\n+\tt-\u003ekcov_df_seq |= KCOV_DF_SEQ_GUARD;\n+\tbarrier();\n+\n+\tarea = (u64 *)t-\u003ekcov_df_area;\n+\tif (!area)\n+\t\tgoto out;\n+\n+\t/* Single-writer exact-count reservation: see kcov_df_reserve(). */\n+\tif (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(2), \u0026start_index))\n+\t\tgoto out;\n+\n+\tseq = kcov_df_next_seq(t);\n+\tarea[start_index]     = kcov_df_hdr(KCOV_DF_TYPE_CMP, 2, 0, 0, seq);\n+\tarea[start_index + 1] = kcov_df_canonicalize_ip(ip);\n+\tarea[start_index + 2] = cmp_type;\n+\tarea[start_index + 3] = arg1;\n+\tarea[start_index + 4] = arg2;\n+out:\n+\tbarrier();\n+\tt-\u003ekcov_df_seq \u0026= ~KCOV_DF_SEQ_GUARD;\n+}\n+EXPORT_SYMBOL(kcov_df_trace_cmp);\n+#endif /* KCOV_DF_HAVE_CMP */\n+\n+/* Called from kernel/fork.c to clear inherited state. */\n+void kcov_dataflow_task_init(struct task_struct *t)\n+{\n+\tt-\u003ekcov_df_area = NULL;\n+\tt-\u003ekcov_df_size = 0;\n+\tt-\u003ekcov_df_seq = 0;\n+\tt-\u003ekcov_df_enabled = false;\n+\tt-\u003ekcov_df = NULL;\n+\tt-\u003ekcov_df_remote_depth = 0;\n+}\n+\n+/* Called from kernel/exit.c to tear down the exiting task's session, if any. */\n+void kcov_dataflow_task_exit(struct task_struct *t)\n+{\n+\tstruct kcov_dataflow *df = t-\u003ekcov_df;\n+\n+\tif (!df)\n+\t\treturn;\n+\n+\tif (t-\u003ekcov_df_remote_depth \u003e 0) {\n+\t\t/*\n+\t\t * A remote kworker exited between kcov_df_remote_start() and\n+\t\t * _stop() (should not happen -- they bracket a single work item).\n+\t\t * Defensive: drop its partial scratch and release the ref so\n+\t\t * neither the buffer nor the object leaks.\n+\t\t */\n+\t\tvoid *scratch = t-\u003ekcov_df_area;\n+\n+\t\tt-\u003ekcov_df_enabled = false;\n+\t\tt-\u003ekcov_df_area = NULL;\n+\t\tt-\u003ekcov_df_size = 0;\n+\t\tt-\u003ekcov_df = NULL;\n+\t\tt-\u003ekcov_df_remote_depth = 0;\n+\t\tvfree(scratch);\n+\t\tkcov_df_put(df);\n+\t\treturn;\n+\t}\n+\n+\t/*\n+\t * Local (KCOV_DF_ENABLE) session on the exiting task. Mirror\n+\t * kcov_task_exit(): unwire the task, clear df-\u003et so the object never\n+\t * keeps a pointer to a freed task_struct (which a later ioctl or\n+\t * close() would compare against current), release the cmp key this\n+\t * session held and drop the session's reference.\n+\t */\n+\tt-\u003ekcov_df_enabled = false;\n+\tt-\u003ekcov_df_area = NULL;\n+\tt-\u003ekcov_df_size = 0;\n+\tt-\u003ekcov_df = NULL;\n+\n+\tmutex_lock(\u0026df-\u003elock);\n+\tWARN_ON_ONCE(df-\u003et != t);\n+\tdf-\u003et = NULL;\n+\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);\n+\tmutex_unlock(\u0026df-\u003elock);\n+\tkcov_df_put(df);\n+}\n+\n+/* File operations for /sys/kernel/debug/kcov_dataflow */\n+\n+static int kcov_df_open(struct inode *inode, struct file *filep)\n+{\n+\tstruct kcov_dataflow *df;\n+\n+\tdf = kzalloc_obj(struct kcov_dataflow, GFP_KERNEL);\n+\tif (!df)\n+\t\treturn -ENOMEM;\n+\tmutex_init(\u0026df-\u003elock);\n+\trefcount_set(\u0026df-\u003erefcount, 1);\t/* the open fd's reference */\n+\tfilep-\u003eprivate_data = df;\n+\treturn nonseekable_open(inode, filep);\n+}\n+\n+/*\n+ * Unwire the local session that @current holds on @df. Caller holds df-\u003elock\n+ * and must drop the session's reference with kcov_df_put() after unlocking.\n+ */\n+static void kcov_df_disable_local(struct kcov_dataflow *df)\n+{\n+\tlockdep_assert_held(\u0026df-\u003elock);\n+\tWARN_ON_ONCE(df-\u003et != current || current-\u003ekcov_df != df);\n+\n+\tcurrent-\u003ekcov_df_enabled = false;\n+\tcurrent-\u003ekcov_df_area = NULL;\n+\tcurrent-\u003ekcov_df_size = 0;\n+\tcurrent-\u003ekcov_df = NULL;\n+\tdf-\u003et = NULL;\n+\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);\n+}\n+\n+static int kcov_df_close(struct inode *inode, struct file *filep)\n+{\n+\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\n+\tbool put_session = false;\n+\n+\t/* Unpublish from remote hash: no new users can start */\n+\tkcov_df_remote_unpublish(df);\n+\n+\tmutex_lock(\u0026df-\u003elock);\n+\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);\n+\t/*\n+\t * Only the enabled task can unwire its own session. If another task\n+\t * (a sibling thread, a fork()ed child, an SCM_RIGHTS recipient) does\n+\t * the final close(), the enabled task keeps its reference and keeps\n+\t * collecting until it exits, exactly like mainline kcov.\n+\t */\n+\tif (df-\u003et == current) {\n+\t\tkcov_df_disable_local(df);\n+\t\tput_session = true;\n+\t}\n+\tmutex_unlock(\u0026df-\u003elock);\n+\n+\tif (put_session)\n+\t\tkcov_df_put(df);\n+\t/*\n+\t * Drop the fd's reference. If remote workers or the enabled task still\n+\t * hold refs, the LAST of them frees -\u003earea via kcov_df_put() -- no drain\n+\t * loop, no lost-decrement wedge. The hash entry was already unpublished\n+\t * above, so no new remote user can start on this object.\n+\t */\n+\tkcov_df_put(df);\n+\treturn 0;\n+}\n+\n+static int kcov_df_mmap(struct file *filep, struct vm_area_struct *vma)\n+{\n+\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\n+\tunsigned long size, off;\n+\tstruct page *page;\n+\tvoid *area;\n+\tint res = 0;\n+\n+\tmutex_lock(\u0026df-\u003elock);\n+\tsize = df-\u003esize * sizeof(u64);\n+\tif (!df-\u003earea || vma-\u003evm_pgoff != 0 ||\n+\t    vma-\u003evm_end - vma-\u003evm_start != size) {\n+\t\tres = -EINVAL;\n+\t\tgoto out;\n+\t}\n+\tarea = df-\u003earea;\n+\tmutex_unlock(\u0026df-\u003elock);\n+\n+\tvm_flags_set(vma, VM_DONTEXPAND);\n+\tfor (off = 0; off \u003c size; off += PAGE_SIZE) {\n+\t\tpage = vmalloc_to_page(area + off);\n+\t\tres = vm_insert_page(vma, vma-\u003evm_start + off, page);\n+\t\tif (res)\n+\t\t\treturn res;\n+\t}\n+\treturn 0;\n+out:\n+\tmutex_unlock(\u0026df-\u003elock);\n+\treturn res;\n+}\n+\n+static long kcov_df_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)\n+{\n+\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\n+\tbool put_session = false;\n+\tunsigned long size;\n+\tu64 handle = 0;\n+\tint res = 0;\n+\n+\t/*\n+\t * Fetch the remote handle from user space before taking df-\u003elock.\n+\t * get_user() may fault and take mmap_lock, but kcov_df_mmap() takes\n+\t * df-\u003elock while holding mmap_lock -- doing the copy under df-\u003elock\n+\t * would invert that order and deadlock (reported by lockdep).\n+\t */\n+\tif (cmd == KCOV_DF_REMOTE_ENABLE \u0026\u0026 get_user(handle, (u64 __user *)arg))\n+\t\treturn -EFAULT;\n+\n+\tmutex_lock(\u0026df-\u003elock);\n+\tswitch (cmd) {\n+\tcase KCOV_DF_INIT_TRACK:\n+\t\tif (df-\u003earea) {\n+\t\t\tres = -EBUSY;\n+\t\t\tbreak;\n+\t\t}\n+\t\tsize = arg;\n+\t\tif (size \u003c 2 || size \u003e (128 \u003c\u003c 20) / sizeof(u64)) {\n+\t\t\tres = -EINVAL;\n+\t\t\tbreak;\n+\t\t}\n+\t\tmutex_unlock(\u0026df-\u003elock);\n+\t\t{\n+\t\t\tvoid *area = vmalloc_user(size * sizeof(u64));\n+\n+\t\t\tif (!area)\n+\t\t\t\treturn -ENOMEM;\n+\t\t\tmutex_lock(\u0026df-\u003elock);\n+\t\t\tif (df-\u003earea) {\n+\t\t\t\tmutex_unlock(\u0026df-\u003elock);\n+\t\t\t\tvfree(area);\n+\t\t\t\treturn -EBUSY;\n+\t\t\t}\n+\t\t\tdf-\u003earea = area;\n+\t\t\tdf-\u003esize = size;\n+\t\t}\n+\t\tbreak;\n+\n+\tcase KCOV_DF_ENABLE:\n+\t\t/*\n+\t\t * One writer per buffer: refuse if this object already has a\n+\t\t * local session, if this task already has one (on any fd), or\n+\t\t * if the buffer is (or may still be) a remote merge target -- a\n+\t\t * published handle, or workers still in flight after\n+\t\t * KCOV_DF_REMOTE_DISABLE (any ref beyond the fd's own). The\n+\t\t * local reservation is a plain read-modify-write of area[0]\n+\t\t * that must never race kcov_df_merge()'s atomic one.\n+\t\t */\n+\t\tif (!df-\u003earea || df-\u003et || df-\u003eremote_handle ||\n+\t\t    refcount_read(\u0026df-\u003erefcount) != 1 || current-\u003ekcov_df) {\n+\t\t\tres = -EBUSY;\n+\t\t\tbreak;\n+\t\t}\n+\t\tkcov_df_fault_in_area(df-\u003earea, df-\u003esize);\n+\t\tkcov_df_get(df);\t/* put in KCOV_DF_DISABLE, close() or task exit */\n+\t\tdf-\u003et = current;\n+\t\tcurrent-\u003ekcov_df = df;\n+\t\tcurrent-\u003ekcov_df_area = df-\u003earea;\n+\t\tcurrent-\u003ekcov_df_size = df-\u003esize;\n+\t\tcurrent-\u003ekcov_df_seq = 0;\n+\t\tcurrent-\u003ekcov_df_remote_depth = 0;\n+\t\t/* Publish the session state before the enable flag. */\n+\t\tbarrier();\n+\t\tcurrent-\u003ekcov_df_enabled = true;\n+\t\tkcov_df_cmp_key_hold(df, KCOV_DF_CMP_LOCAL);\n+\t\tbreak;\n+\n+\tcase KCOV_DF_DISABLE:\n+\t\tif (df-\u003et != current) {\n+\t\t\tres = -EINVAL;\n+\t\t\tbreak;\n+\t\t}\n+\t\tkcov_df_disable_local(df);\n+\t\tput_session = true;\n+\t\tbreak;\n+\n+\tcase KCOV_DF_REMOTE_ENABLE: {\n+\t\tstruct kcov_df_remote *remote;\n+\n+\t\tif (!df-\u003earea ||\n+\t\t    !kcov_check_handle(handle, true, true, false)) {\n+\t\t\tres = -EINVAL;\n+\t\t\tbreak;\n+\t\t}\n+\t\t/*\n+\t\t * One handle per fd (a second one would leak the first entry\n+\t\t * and leave it pointing at a freed object after close()), and\n+\t\t * never while a local session writes the buffer directly.\n+\t\t */\n+\t\tif (df-\u003et || df-\u003eremote_handle) {\n+\t\t\tres = -EBUSY;\n+\t\t\tbreak;\n+\t\t}\n+\t\tremote = kzalloc_obj(struct kcov_df_remote, GFP_KERNEL);\n+\t\tif (!remote) {\n+\t\t\tres = -ENOMEM;\n+\t\t\tbreak;\n+\t\t}\n+\t\tremote-\u003ehandle = handle;\n+\t\tremote-\u003edf = df;\n+\t\tmutex_lock(\u0026kcov_df_remote_lock);\n+\t\tif (kcov_df_remote_find(handle)) {\n+\t\t\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\t\t\tkfree(remote);\n+\t\t\tres = -EEXIST;\n+\t\t\tbreak;\n+\t\t}\n+\t\thash_add(kcov_df_remote_map, \u0026remote-\u003ehnode, handle);\n+\t\tdf-\u003eremote_handle = handle;\n+\t\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\t\tkcov_df_cmp_key_hold(df, KCOV_DF_CMP_REMOTE);\n+\t\tbreak;\n+\t}\n+\n+\tcase KCOV_DF_REMOTE_DISABLE:\n+\t\tkcov_df_remote_unpublish(df);\n+\t\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tres = -ENOTTY;\n+\t}\n+\tmutex_unlock(\u0026df-\u003elock);\n+\n+\tif (put_session)\n+\t\tkcov_df_put(df);\n+\treturn res;\n+}\n+\n+/* Remote dataflow implementation */\n+\n+/*\n+ * Open a remote dataflow section on this task for @handle. Must be called from\n+ * sleepable task context (it takes a mutex and may vmalloc() the scratch); in\n+ * softirq/hardirq context it is a no-op, as is the matching stop, so the pair\n+ * stays balanced for a call site that brackets a softirq-reachable region.\n+ */\n+void kcov_df_remote_start(u64 handle)\n+{\n+\tstruct kcov_df_remote *remote;\n+\tstruct kcov_dataflow *df;\n+\tvoid *scratch;\n+\n+\t/* Dataflow remote coverage is collected in task (kworker) context only. */\n+\tif (!in_task())\n+\t\treturn;\n+\t/*\n+\t * A task should only run one session at a time (KCOV's rule). If a\n+\t * buggy caller nests inside a remote section, don't re-init and don't\n+\t * take a second ref -- just count the depth so the matching inner\n+\t * stop() leaves the outer session intact (see kcov_df_remote_stop()).\n+\t * Coverage from the nested region is attributed to the outer handle,\n+\t * which is safe (no corruption, no early free) even though it is\n+\t * imprecise. Inside a local (KCOV_DF_ENABLE) session the depth stays\n+\t * 0, so the inner stop() is a no-op and the local session's wiring is\n+\t * left untouched; its records simply go to its own buffer.\n+\t *\n+\t * This check comes first so that every early return below only ever\n+\t * happens with no session live -- then the matching stop() has nothing\n+\t * to tear down and can never truncate an outer section.\n+\t */\n+\tif (current-\u003ekcov_df) {\n+\t\tWARN_ON_ONCE(1);\n+\t\tif (current-\u003ekcov_df_remote_depth \u003e 0 \u0026\u0026\n+\t\t    current-\u003ekcov_df_remote_depth \u003c INT_MAX)\n+\t\t\tcurrent-\u003ekcov_df_remote_depth++;\n+\t\treturn;\n+\t}\n+\tif (!handle)\n+\t\treturn;\n+\n+\t/* mutex_lock()'s might_sleep() reports an atomic (non-sleepable) caller. */\n+\tmutex_lock(\u0026kcov_df_remote_lock);\n+\tremote = kcov_df_remote_find(handle);\n+\tif (!remote || !remote-\u003edf || !remote-\u003edf-\u003earea) {\n+\t\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\t\treturn;\n+\t}\n+\tdf = remote-\u003edf;\n+\tkcov_df_get(df);\t\t/* keep @df (and -\u003earea) alive until _stop() */\n+\tscratch = kcov_df_scratch_get();\t/* reuse a pooled scratch if any */\n+\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\n+\tif (!scratch) {\n+\t\tscratch = vmalloc(KCOV_DF_REMOTE_WORDS * sizeof(u64));\n+\t\tif (!scratch) {\n+\t\t\tkcov_df_put(df);\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\t((u64 *)scratch)[0] = 0;\t/* reset the scratch write cursor */\n+\tkcov_df_fault_in_area(scratch, KCOV_DF_REMOTE_WORDS);\n+\n+\t/*\n+\t * Point this task at its OWN private scratch, NOT df-\u003earea. It collects\n+\t * here while it runs; kcov_df_remote_stop() merges it into the shared\n+\t * buffer. So multiple kworkers on one handle never write the same buffer.\n+\t */\n+\tcurrent-\u003ekcov_df_area = scratch;\n+\tcurrent-\u003ekcov_df_size = KCOV_DF_REMOTE_WORDS;\n+\tcurrent-\u003ekcov_df_seq = 0;\n+\tcurrent-\u003ekcov_df = df;\t\t/* pocket it for _stop(); no hash relookup */\n+\tcurrent-\u003ekcov_df_remote_depth = 1;\n+\t/*\n+\t * Publish all session state BEFORE the enable flag (mirrors kcov_start()).\n+\t * kcov_df_write() gates on kcov_df_enabled and then reads kcov_df_area, so\n+\t * the buffer/handle must be visible first; the barrier keeps the compiler\n+\t * from hoisting the enable above them.\n+\t */\n+\tbarrier();\n+\tcurrent-\u003ekcov_df_enabled = true;\n+}\n+EXPORT_SYMBOL_GPL(kcov_df_remote_start);\n+\n+void kcov_df_remote_stop(void)\n+{\n+\tstruct kcov_dataflow *df = current-\u003ekcov_df;\n+\tvoid *scratch;\n+\n+\t/*\n+\t * Same context rule as kcov_df_remote_start(): a stop() in softirq\n+\t * context pairs with a start() that was a no-op, and must not touch\n+\t * the interrupted task's live session.\n+\t */\n+\tif (!in_task())\n+\t\treturn;\n+\t/* No remote session (a local session ignores a stray stop). */\n+\tif (!df || current-\u003ekcov_df_remote_depth == 0)\n+\t\treturn;\n+\n+\t/*\n+\t * Unwind a nested start() (buggy caller): only the OUTERMOST stop tears\n+\t * the session down. Inner stops just decrement the depth and return, so\n+\t * the buffer/ref survive until the worker is really done with them.\n+\t */\n+\tif (--current-\u003ekcov_df_remote_depth \u003e 0)\n+\t\treturn;\n+\n+\tscratch = current-\u003ekcov_df_area;\n+\n+\t/*\n+\t * Stop writing FIRST: clear the per-task pointers so this task can no\n+\t * longer enter kcov_df_write() / touch the scratch. Then it is safe to\n+\t * merge and recycle the scratch and drop the ref.\n+\t */\n+\tcurrent-\u003ekcov_df_enabled = false;\n+\tcurrent-\u003ekcov_df_area = NULL;\n+\tcurrent-\u003ekcov_df_size = 0;\n+\tcurrent-\u003ekcov_df = NULL;\n+\n+\tif (scratch) {\n+\t\t/*\n+\t\t * Publish this worker's records into the shared buffer,\n+\t\t * then return the scratch to the pool for the next worker.\n+\t\t */\n+\t\tkcov_df_merge(df, scratch);\n+\t\tmutex_lock(\u0026kcov_df_remote_lock);\n+\t\tkcov_df_scratch_put(scratch);\n+\t\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\t}\n+\n+\t/*\n+\t * Drop the ref taken in kcov_df_remote_start(). If this is the last one,\n+\t * kcov_df_put() frees -\u003earea right here -- safe, because no task writes\n+\t * -\u003earea directly anymore (workers write scratch; the merge above is\n+\t * done). Dropping via the pocketed @df (not a hash lookup) means an\n+\t * already-unpublished entry can never strand the count.\n+\t */\n+\tkcov_df_put(df);\n+}\n+EXPORT_SYMBOL_GPL(kcov_df_remote_stop);\n+\n+static const struct file_operations kcov_df_fops = {\n+\t.open\t\t= kcov_df_open,\n+\t.unlocked_ioctl\t= kcov_df_ioctl,\n+\t.compat_ioctl\t= kcov_df_ioctl,\n+\t.mmap\t\t= kcov_df_mmap,\n+\t.release\t= kcov_df_close,\n+};\n+\n+/*\n+ * Reclaim idle per-worker scratch under memory pressure. The pool otherwise only\n+ * ever grows to the peak number of concurrent remote kworkers (each area is 8 MiB)\n+ * and is never returned to the allocator; a shrinker lets the VM take the idle\n+ * (parked) areas back when it needs the memory. Only pooled areas are freeable;\n+ * in-use scratch is not on the list. mutex_trylock keeps the shrinker best-effort\n+ * and free of any lock-ordering risk.\n+ */\n+static unsigned long\n+kcov_df_scratch_shrink_count(struct shrinker *sh, struct shrink_control *sc)\n+{\n+\tunsigned long nr;\n+\n+\tif (!mutex_trylock(\u0026kcov_df_remote_lock))\n+\t\treturn 0;\n+\tnr = kcov_df_scratch_pool_nr;\n+\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\treturn nr ? nr : SHRINK_EMPTY;\n+}\n+\n+static unsigned long\n+kcov_df_scratch_shrink_scan(struct shrinker *sh, struct shrink_control *sc)\n+{\n+\tstruct kcov_df_scratch *s, *tmp;\n+\tLIST_HEAD(victims);\n+\tunsigned long freed = 0;\n+\n+\tif (!mutex_trylock(\u0026kcov_df_remote_lock))\n+\t\treturn SHRINK_STOP;\n+\t/*\n+\t * Detach victims under the lock; free them (each 8 MiB) after unlocking\n+\t * so the vfree() latency stays off concurrent remote_start()/stop().\n+\t */\n+\twhile (freed \u003c sc-\u003enr_to_scan \u0026\u0026 !list_empty(\u0026kcov_df_scratch_pool)) {\n+\t\ts = list_first_entry(\u0026kcov_df_scratch_pool,\n+\t\t\t\t     struct kcov_df_scratch, list);\n+\t\tlist_move(\u0026s-\u003elist, \u0026victims);\n+\t\tkcov_df_scratch_pool_nr--;\n+\t\tfreed++;\n+\t}\n+\tmutex_unlock(\u0026kcov_df_remote_lock);\n+\n+\tlist_for_each_entry_safe(s, tmp, \u0026victims, list)\n+\t\tvfree(s);\n+\treturn freed;\n+}\n+\n+static int __init kcov_dataflow_init(void)\n+{\n+\tstruct shrinker *shrinker;\n+\n+\tdebugfs_create_file_unsafe(\"kcov_dataflow\", 0600, NULL, NULL,\n+\t\t\t\t   \u0026kcov_df_fops);\n+\n+\tshrinker = shrinker_alloc(0, \"kcov-df-scratch\");\n+\tif (shrinker) {\n+\t\tshrinker-\u003ecount_objects = kcov_df_scratch_shrink_count;\n+\t\tshrinker-\u003escan_objects = kcov_df_scratch_shrink_scan;\n+\t\tshrinker-\u003eseeks = DEFAULT_SEEKS;\n+\t\tshrinker_register(shrinker);\n+\t} else {\n+\t\tpr_warn(\"scratch shrinker unavailable, idle remote scratch areas will not be reclaimed\\n\");\n+\t}\n+\treturn 0;\n+}\n+device_initcall(kcov_dataflow_init);\ndiff --git a/lib/Kconfig.debug b/lib/Kconfig.debug\nindex 134b15a44625e..6b724ae713ce1 100644\n--- a/lib/Kconfig.debug\n+++ b/lib/Kconfig.debug\n@@ -2219,6 +2219,58 @@ config KCOV_SELFTEST\n \t  On test failure, causes the kernel to panic. Recommended to be\n \t  enabled, ensuring critical functionality works as intended.\n \n+config KCOV_DATAFLOW_ARGS\n+\tbool \"Enable KCOV dataflow: function argument capture\"\n+\tdepends on KCOV\n+\tdepends on CC_IS_CLANG\n+\tdepends on DEBUG_INFO\n+\tdepends on $(cc-option,-fsanitize-coverage=trace-args)\n+\tdepends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-args)\n+\thelp\n+\t  Captures function arguments at entry via /sys/kernel/debug/kcov_dataflow.\n+\t  Struct pointer arguments are auto-expanded using compiler DebugInfo\n+\t  metadata, recording individual field values at runtime.\n+\t  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.\n+\t  Requires clang with -fsanitize-coverage=trace-args support (and,\n+\t  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus\n+\t  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under\n+\t  \"Compile-time checks and compiler options\" to satisfy DEBUG_INFO.\n+\n+config KCOV_DATAFLOW_RET\n+\tbool \"Enable KCOV dataflow: return value capture\"\n+\tdepends on KCOV\n+\tdepends on CC_IS_CLANG\n+\tdepends on DEBUG_INFO\n+\tdepends on $(cc-option,-fsanitize-coverage=trace-ret)\n+\tdepends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-ret)\n+\thelp\n+\t  Captures function return values via /sys/kernel/debug/kcov_dataflow.\n+\t  Struct pointer returns are auto-expanded using compiler DebugInfo\n+\t  metadata, recording individual field values at runtime.\n+\t  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.\n+\t  Requires clang with -fsanitize-coverage=trace-ret support (and,\n+\t  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus\n+\t  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under\n+\t  \"Compile-time checks and compiler options\" to satisfy DEBUG_INFO.\n+\n+config KCOV_DATAFLOW_NO_INLINE\n+\tbool \"Disable inlining for dataflow-instrumented files\"\n+\tdepends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET\n+\thelp\n+\t  Adds -fno-inline to files instrumented with KCOV_DATAFLOW.\n+\t  This ensures every function boundary is preserved, giving\n+\t  complete argument visibility. Disable for lower overhead at the\n+\t  cost of losing argument records for inlined functions.\n+\n+config KCOV_DATAFLOW_INSTRUMENT_ALL\n+\tbool \"Instrument all kernel code with dataflow coverage\"\n+\tdepends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET\n+\thelp\n+\t  Instrument all kernel objects with trace-args/trace-ret\n+\t  automatically. Individual files or directories can opt out\n+\t  with KCOV_DATAFLOW_file.o := n or KCOV_DATAFLOW := n.\n+\t  Warning: significantly increases code size and boot time.\n+\n menuconfig RUNTIME_TESTING_MENU\n \tbool \"Runtime Testing\"\n \tdefault y\ndiff --git a/scripts/Makefile.kcov b/scripts/Makefile.kcov\nindex 78305a84ba9d2..5fd2aa69d8fd5 100644\n--- a/scripts/Makefile.kcov\n+++ b/scripts/Makefile.kcov\n@@ -9,3 +9,20 @@ kcov-rflags-$(CONFIG_KCOV_ENABLE_COMPARISONS)\t+= -Cllvm-args=-sanitizer-coverage\n \n export CFLAGS_KCOV := $(kcov-flags-y)\n export RUSTFLAGS_KCOV := $(kcov-rflags-y)\n+\n+# KCOV dataflow: trace function args and return values. Each kind is gated by\n+# its own Kconfig symbol, matching the #ifdef around the callback it emits calls\n+# to in kernel/kcov_dataflow.c (an instrumented object must never reference a\n+# callback that is not compiled in). Both variables are empty on a KCOV-only\n+# kernel, so a stray per-file KCOV_DATAFLOW_file.o := y is harmless there.\n+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -fsanitize-coverage=trace-args\n+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_RET) += -fsanitize-coverage=trace-ret\n+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_NO_INLINE) += -fno-inline\n+\n+# Rust: only add the trace-args/ret llvm-args (sancov-module pass and level=3\n+# are already provided by RUSTFLAGS_KCOV since KCOV_DATAFLOW depends on KCOV).\n+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -Cllvm-args=-sanitizer-coverage-trace-args\n+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_RET) += -Cllvm-args=-sanitizer-coverage-trace-ret\n+\n+export CFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-flags-y)\n+export RUSTFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-rflags-y)\ndiff --git a/scripts/Makefile.lib b/scripts/Makefile.lib\nindex 0a4fdd8bd975d..b32fa67ce99af 100644\n--- a/scripts/Makefile.lib\n+++ b/scripts/Makefile.lib\n@@ -88,6 +88,20 @@ _c_flags += $(if $(patsubst n%,, \\\n _rust_flags += $(if $(patsubst n%,, \\\n \t$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)$(if $(is-kernel-object),$(CONFIG_KCOV_INSTRUMENT_ALL))), \\\n \t$(RUSTFLAGS_KCOV))\n+# KCOV dataflow. The outer test only honours an explicit KCOV opt-out\n+# (KCOV_INSTRUMENT_file.o := n / KCOV_INSTRUMENT := n, the noinstr exclusions):\n+# it does not require a KCOV opt-in, so per-file KCOV_DATAFLOW_file.o := y works\n+# for modules and out-of-tree objects too. The inner test is the dataflow opt-in:\n+# per-file/per-directory, or every kernel object under\n+# CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL.\n+_c_flags += $(if $(patsubst n%,, \\\n+\t$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \\\n+\t$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \\\n+\t$(CFLAGS_KCOV_DATAFLOW)))\n+_rust_flags += $(if $(patsubst n%,, \\\n+\t$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \\\n+\t$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \\\n+\t$(RUSTFLAGS_KCOV_DATAFLOW)))\n endif\n \n #\ndiff --git a/tools/objtool/check.c b/tools/objtool/check.c\nindex 464f6c9d9ff0b..ae6fe47886395 100644\n--- a/tools/objtool/check.c\n+++ b/tools/objtool/check.c\n@@ -1219,6 +1219,10 @@ static const char *uaccess_safe_builtin[] = {\n \t\"__tsan_unaligned_write16\",\n \t/* KCOV */\n \t\"write_comp_data\",\n+\t/* KCOV dataflow */\n+\t\"kcov_df_trace_cmp\",\n+\t\"__sanitizer_cov_trace_args\",\n+\t\"__sanitizer_cov_trace_ret\",\n \t\"check_kcov_mode\",\n \t\"__sanitizer_cov_trace_pc\",\n \t\"__sanitizer_cov_trace_const_cmp1\",\ndiff --git a/tools/testing/selftests/kcov_dataflow/.gitignore b/tools/testing/selftests/kcov_dataflow/.gitignore\nnew file mode 100644\nindex 0000000000000..4f2957a017957\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/.gitignore\n@@ -0,0 +1,4 @@\n+# SPDX-License-Identifier: GPL-2.0\n+user_ioctl/user_ioctl\n+binderfs/binderfs_test\n+__pycache__/\ndiff --git a/tools/testing/selftests/kcov_dataflow/Kbuild b/tools/testing/selftests/kcov_dataflow/Kbuild\nnew file mode 100644\nindex 0000000000000..2e19e9008fdca\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/Kbuild\n@@ -0,0 +1,10 @@\n+# SPDX-License-Identifier: GPL-2.0\n+#\n+# Test modules, built as external modules against the configured kernel tree\n+# by the selftest Makefile (\"make -C $(KDIR) M=$(CURDIR) modules\"). Every\n+# directory opts its object into dataflow instrumentation with\n+# KCOV_DATAFLOW_\u003cobject\u003e.o := y, the same per-file switch in-tree code uses.\n+obj-m\t\t\t\t+= rust_ffi_contract/\n+obj-m\t\t\t\t+= eight_struct_args_c/\n+obj-$(CONFIG_RUST)\t\t+= eight_struct_args_rust/\n+obj-$(CONFIG_RUST)\t\t+= rust_kworker_remote/\ndiff --git a/tools/testing/selftests/kcov_dataflow/Makefile b/tools/testing/selftests/kcov_dataflow/Makefile\nnew file mode 100644\nindex 0000000000000..fc979e2d4ecc3\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/Makefile\n@@ -0,0 +1,46 @@\n+# SPDX-License-Identifier: GPL-2.0\n+#\n+# kcov_dataflow selftests\n+#\n+# user_ioctl and binderfs are ordinary kselftest programs. The test modules\n+# (one per directory, listed in Kbuild) are built by kbuild against KDIR and\n+# are loaded, triggered and checked by test_modules.py; trigger-view.py is\n+# the interactive viewer the runner is built on.\n+#\n+# KDIR is the configured kernel build tree. It defaults to the source tree\n+# this directory lives in; point it at the O= directory for out-of-tree\n+# builds. Pass the same LLVM=1 CC=clang [RUSTC= RUST_LIB_SRC=] the kernel was\n+# built with so that kbuild picks the toolchain that has the trace-args and\n+# trace-ret passes.\n+KDIR ?= $(abspath ../../../..)\n+\n+TEST_GEN_PROGS := user_ioctl/user_ioctl binderfs/binderfs_test\n+TEST_PROGS := test_modules.py\n+TEST_FILES := trigger-view.py\n+\n+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)\n+\n+# The .ko files kbuild produces for KDIR's configuration, so that they are\n+# built by \"all\" and copied by \"install\"; the Rust modules need CONFIG_RUST.\n+KMODS := rust_ffi_contract eight_struct_args_c\n+ifneq ($(shell grep -s ^CONFIG_RUST=y $(KDIR)/.config),)\n+KMODS += eight_struct_args_rust rust_kworker_remote\n+endif\n+TEST_GEN_FILES := $(foreach m,$(KMODS),$(m)/$(m).ko)\n+\n+include ../lib.mk\n+\n+ifneq ($(wildcard $(KDIR)/.config),)\n+$(TEST_GEN_FILES): modules\n+modules:\n+\t$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) modules\n+clean_modules:\n+\t$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) clean\n+else\n+$(TEST_GEN_FILES):\n+\t@echo \"SKIP $(notdir $@): no configured kernel tree at $(KDIR), set KDIR=\"\n+clean_modules:\n+endif\n+\n+clean: clean_modules\n+.PHONY: modules clean_modules\ndiff --git a/tools/testing/selftests/kcov_dataflow/README.rst b/tools/testing/selftests/kcov_dataflow/README.rst\nnew file mode 100644\nindex 0000000000000..1929a357aca47\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/README.rst\n@@ -0,0 +1,69 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests\n+=======================\n+\n+Selftests for ``/sys/kernel/debug/kcov_dataflow`` (see\n+Documentation/dev-tools/kcov-dataflow.rst).\n+\n+Layout\n+------\n+\n+Makefile, Kbuild\n+    kselftest build: the C programs are built by lib.mk, the test modules\n+    (one directory each, listed in Kbuild) by kbuild against ``KDIR``.\n+user_ioctl/\n+    ioctl interface test (kselftest harness, TAP).\n+binderfs/\n+    binder ioctls under recording (TAP).\n+rust_ffi_contract/, eight_struct_args_c/, eight_struct_args_rust/,\n+rust_kworker_remote/\n+    test modules; each README.rst says what the module exercises.\n+test_modules.py\n+    KTAP runner: loads every module, triggers it with recording active and\n+    checks the captured arguments, struct fields and return values against\n+    the values the module uses. Modules that are not built are SKIPped.\n+trigger-view.py\n+    Interactive viewer the runner is built on (call tree or ``--raw``\n+    records, kallsyms/addr2line symbolization, ``--remote`` capture).\n+\n+Kernel\n+------\n+\n+The kernel and the modules must be built with a clang that has the\n+trace-args/trace-ret passes (and, for the Rust modules, a rustc built\n+against that LLVM). The config fragment ``config`` lists what the tests\n+need; with virtme-ng::\n+\n+    vng --build --config tools/testing/selftests/kcov_dataflow/config \\\n+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC\n+\n+Build\n+-----\n+\n+From the kernel tree, with the same toolchain variables::\n+\n+    make LLVM=1 headers\n+    make -C tools/testing/selftests TARGETS=kcov_dataflow \\\n+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC\n+\n+``KDIR`` defaults to the source tree; pass ``KDIR=\u003cO dir\u003e`` for out-of-tree\n+builds. The Rust modules are built only when ``KDIR/.config`` has\n+``CONFIG_RUST=y``. ``make ... install INSTALL_PATH=\u003cdir\u003e`` produces a\n+self-contained tree with ``run_kselftest.sh``.\n+\n+Run\n+---\n+\n+On the target (root, debugfs mounted)::\n+\n+    vng --user root --exec \\\n+        \"tools/testing/selftests/kcov_dataflow/test_modules.py\"\n+    tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\n+    tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\n+\n+or, from an installed tree, ``run_kselftest.sh -c kcov_dataflow``.\n+``test_modules.py -t \u003cmodule\u003e -C 8`` runs one module and echoes eight\n+records of context around each module record; ``trigger-view.py \u003cmodule\u003e\n+[--raw] [-C N] [--remote] [--vmlinux vmlinux]`` shows the capture\n+without checking it.\ndiff --git a/tools/testing/selftests/kcov_dataflow/binderfs/Makefile b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile\nnew file mode 100644\nindex 0000000000000..b35de62649924\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile\n@@ -0,0 +1,5 @@\n+# SPDX-License-Identifier: GPL-2.0\n+# Standalone build of the binderfs test: make -C tools/testing/selftests/kcov_dataflow/binderfs\n+TEST_GEN_PROGS := binderfs_test\n+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)\n+include ../../lib.mk\ndiff --git a/tools/testing/selftests/kcov_dataflow/binderfs/README.rst b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst\nnew file mode 100644\nindex 0000000000000..7fcdce1955c19\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst\n@@ -0,0 +1,13 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: binderfs\n+=================================\n+\n+Exercises the binder driver via binderfs with kcov_dataflow recording\n+active and verifies that argument records are captured at the binder\n+ioctl boundaries. Needs CONFIG_ANDROID_BINDERFS=y and binder instrumented\n+(``KCOV_DATAFLOW := y`` in drivers/android/Makefile or\n+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y); SKIPs without binderfs::\n+\n+  make -C tools/testing/selftests TARGETS=kcov_dataflow\n+  tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\ndiff --git a/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c\nnew file mode 100644\nindex 0000000000000..650798e09b20a\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c\n@@ -0,0 +1,195 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * binderfs selftest for kcov_dataflow\n+ *\n+ * Exercises the binder driver via binderfs with kcov_dataflow recording\n+ * active, then verifies that function argument records were captured at\n+ * binder ioctl boundaries.\n+ *\n+ * Requires: CONFIG_ANDROID_BINDER_IPC=y (or _RUST), CONFIG_ANDROID_BINDERFS=y\n+ */\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003cerrno.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/mount.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003clinux/android/binder.h\u003e\n+#include \u003clinux/android/binderfs.h\u003e\n+#include \u003clinux/kcov_dataflow.h\u003e\n+\n+\n+#define BUF_SIZE\t(1 \u003c\u003c 20)\n+#define BINDERFS_PATH\t\"/tmp/binderfs_test\"\n+#define BINDER_DEV\tBINDERFS_PATH \"/my_binder\"\n+\n+static int setup_binderfs(void)\n+{\n+\tstruct binderfs_device dev = {};\n+\n+\tmkdir(BINDERFS_PATH, 0755);\n+\n+\tif (mount(\"binder\", BINDERFS_PATH, \"binder\", 0, NULL)) {\n+\t\tif (errno == ENODEV || errno == ENOENT) {\n+\t\t\tprintf(\"SKIP: binderfs not available\\n\");\n+\t\t\treturn -1;\n+\t\t}\n+\t\tperror(\"mount binderfs\");\n+\t\treturn -1;\n+\t}\n+\n+\t/* Create a binder device via BINDER_CTL_ADD ioctl */\n+\tint ctl_fd;\n+\n+\tctl_fd = open(BINDERFS_PATH \"/binder-control\", O_RDONLY);\n+\tif (ctl_fd \u003c 0) {\n+\t\tperror(\"open binder-control\");\n+\t\tumount(BINDERFS_PATH);\n+\t\treturn -1;\n+\t}\n+\n+\tstrcpy(dev.name, \"my_binder\");\n+\tif (ioctl(ctl_fd, BINDER_CTL_ADD, \u0026dev) \u0026\u0026 errno != EEXIST) {\n+\t\tperror(\"BINDER_CTL_ADD\");\n+\t\tclose(ctl_fd);\n+\t\tumount(BINDERFS_PATH);\n+\t\treturn -1;\n+\t}\n+\tclose(ctl_fd);\n+\treturn 0;\n+}\n+\n+static void cleanup_binderfs(void)\n+{\n+\tumount(BINDERFS_PATH);\n+\trmdir(BINDERFS_PATH);\n+}\n+\n+int main(void)\n+{\n+\tuint64_t *buf;\n+\tint df_fd, binder_fd;\n+\tuint64_t total;\n+\tint valid = 0;\n+\n+\tprintf(\"TAP version 13\\n\");\n+\tprintf(\"1..3\\n\");\n+\n+\t/* Setup binderfs */\n+\tif (setup_binderfs()) {\n+\t\tprintf(\"ok 1 # SKIP binderfs not available\\n\");\n+\t\tprintf(\"ok 2 # SKIP\\n\");\n+\t\tprintf(\"ok 3 # SKIP\\n\");\n+\t\treturn 0;\n+\t}\n+\n+\t/* Open kcov_dataflow */\n+\tdf_fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+\tif (df_fd \u003c 0) {\n+\t\tprintf(\"not ok 1 cannot open kcov_dataflow\\n\");\n+\t\tcleanup_binderfs();\n+\t\treturn 1;\n+\t}\n+\n+\tif (ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)) {\n+\t\tprintf(\"not ok 1 INIT_TRACK failed\\n\");\n+\t\tclose(df_fd);\n+\t\tcleanup_binderfs();\n+\t\treturn 1;\n+\t}\n+\n+\tbuf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+\t\t   PROT_READ | PROT_WRITE, MAP_SHARED, df_fd, 0);\n+\tif (buf == MAP_FAILED) {\n+\t\tprintf(\"not ok 1 mmap failed\\n\");\n+\t\tclose(df_fd);\n+\t\tcleanup_binderfs();\n+\t\treturn 1;\n+\t}\n+\n+\tprintf(\"ok 1 kcov_dataflow.binderfs_setup\\n\");\n+\n+\t/* Open binder device */\n+\tbinder_fd = open(BINDER_DEV, O_RDWR | O_CLOEXEC);\n+\tif (binder_fd \u003c 0) {\n+\t\tprintf(\"not ok 2 cannot open %s: %s\\n\", BINDER_DEV,\n+\t\t       strerror(errno));\n+\t\tmunmap(buf, BUF_SIZE * sizeof(uint64_t));\n+\t\tclose(df_fd);\n+\t\tcleanup_binderfs();\n+\t\treturn 1;\n+\t}\n+\n+\t/* Enable recording and exercise binder ioctls */\n+\tioctl(df_fd, KCOV_DF_ENABLE, 0);\n+\t__atomic_store_n(\u0026buf[0], 0, __ATOMIC_RELAXED);\n+\n+\t/* BINDER_VERSION - simple ioctl that exercises the binder path */\n+\tstruct binder_version ver = {};\n+\n+\tioctl(binder_fd, BINDER_VERSION, \u0026ver);\n+\n+\t/* BINDER_SET_MAX_THREADS */\n+\tuint32_t max_threads = 4;\n+\n+\tioctl(binder_fd, BINDER_SET_MAX_THREADS, \u0026max_threads);\n+\n+\tioctl(df_fd, KCOV_DF_DISABLE, 0);\n+\n+\ttotal = __atomic_load_n(\u0026buf[0], __ATOMIC_RELAXED);\n+\tclose(binder_fd);\n+\n+\tif (total \u003e 0)\n+\t\tprintf(\"ok 2 kcov_dataflow.binderfs_captured # %lu words\\n\",\n+\t\t       (unsigned long)total);\n+\telse\n+\t\tprintf(\"not ok 2 kcov_dataflow.binderfs_captured # 0 words\\n\");\n+\n+\t/*\n+\t * Walk the records: every header must carry a known type and at least\n+\t * one value word, the walk must end exactly at area[0], and at least one\n+\t * ENTRY/RET record must come from the binder ioctls (CMP records are\n+\t * interleaved with CONFIG_KCOV_ENABLE_COMPARISONS=y).\n+\t */\n+\tif (total \u003c= BUF_SIZE - 1) {\n+\t\tuint64_t pos = 1, end = 1 + total;\n+\t\tunsigned long nargs = 0;\n+\n+\t\twhile (pos + KCOV_DF_RECORD_HDR_WORDS \u003c= end) {\n+\t\t\tuint64_t hdr = buf[pos];\n+\t\t\tuint32_t type = KCOV_DF_HDR_TYPE(hdr);\n+\t\t\tuint32_t nvals = KCOV_DF_HDR_NVALS(hdr);\n+\n+\t\t\tif (nvals \u003c 1 || (type != KCOV_DF_TYPE_ENTRY \u0026\u0026\n+\t\t\t\t\t  type != KCOV_DF_TYPE_RET \u0026\u0026\n+\t\t\t\t\t  type != KCOV_DF_TYPE_CMP))\n+\t\t\t\tbreak;\n+\t\t\tif (type != KCOV_DF_TYPE_CMP)\n+\t\t\t\tnargs++;\n+\t\t\tpos += KCOV_DF_RECORD_WORDS(nvals);\n+\t\t}\n+\t\tif (pos == end \u0026\u0026 nargs \u003e 0)\n+\t\t\tvalid = 1;\n+\t\telse\n+\t\t\tprintf(\"# walk stopped at word %lu of %lu, %lu ENTRY/RET records\\n\",\n+\t\t\t       (unsigned long)pos, (unsigned long)end, nargs);\n+\t}\n+\n+\tif (valid)\n+\t\tprintf(\"ok 3 kcov_dataflow.binderfs_valid_records\\n\");\n+\telse\n+\t\tprintf(\"not ok 3 kcov_dataflow.binderfs_valid_records\\n\");\n+\n+\tprintf(\"# Totals: pass:%d fail:%d skip:0\\n\",\n+\t       valid ? 3 : 2, valid ? 0 : 1);\n+\n+\tmunmap(buf, BUF_SIZE * sizeof(uint64_t));\n+\tclose(df_fd);\n+\tcleanup_binderfs();\n+\treturn valid ? 0 : 1;\n+}\ndiff --git a/tools/testing/selftests/kcov_dataflow/config b/tools/testing/selftests/kcov_dataflow/config\nnew file mode 100644\nindex 0000000000000..7f3a2fda0641d\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/config\n@@ -0,0 +1,11 @@\n+CONFIG_KCOV=y\n+CONFIG_KCOV_DATAFLOW_ARGS=y\n+CONFIG_KCOV_DATAFLOW_RET=y\n+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y\n+CONFIG_KCOV_DATAFLOW_NO_INLINE=y\n+CONFIG_DEBUG_INFO_DWARF5=y\n+CONFIG_DEBUG_FS=y\n+CONFIG_MODULES=y\n+CONFIG_ANDROID_BINDER_IPC=y\n+CONFIG_ANDROID_BINDERFS=y\n+CONFIG_RUST=y\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile\nnew file mode 100644\nindex 0000000000000..04ff83f0a9625\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile\n@@ -0,0 +1,3 @@\n+# SPDX-License-Identifier: GPL-2.0\n+obj-m := eight_struct_args_c.o\n+KCOV_DATAFLOW_eight_struct_args_c.o := y\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst\nnew file mode 100644\nindex 0000000000000..62cddee78cd36\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst\n@@ -0,0 +1,13 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: eight_struct_args_c\n+============================================\n+\n+C module with 1-8 struct pointer arguments (flat s1..s8), value-nested\n+st1..st8 and pointer-linked stp1..stp8 towers (on stack, kmalloc and\n+vmalloc), pointer forwarding and a struct return value. Opted in with\n+``KCOV_DATAFLOW_eight_struct_args_c.o := y``; test_modules.py checks the\n+expanded fields (0x11, 0x22, ...) and every return value::\n+\n+  ./test_modules.py -t eight_struct_args_c\n+  ./trigger-view.py eight_struct_args_c --raw\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c\nnew file mode 100644\nindex 0000000000000..c7d06a8e94c38\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c\n@@ -0,0 +1,533 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * eight_struct_args_c.c - Verify kcov_dataflow captures struct pointer\n+ * arguments with automatic field expansion.\n+ *\n+ * Three families of structs are exercised:\n+ *\n+ *  - Flat structs s1..s8: sN has N u64 members side by side; sf_N takes N\n+ *    struct pointer args (s1*..sN*). Tests plain field expansion and multiple\n+ *    struct-pointer arguments.\n+ *\n+ *  - Recursively (value) nested structs st1..st8: stN embeds every smaller\n+ *    struct by value, so the nesting deepens with N:\n+ *        st1 = { u64 field0 }\n+ *        st2 = { u64 field0, st1 field1 }             // { v, {v} }\n+ *        stN = { u64 field0, st1 field1, ... st(N-1) field(N-1) }\n+ *    The deepest chain in st8 is eight levels deep. Used by the stack tests.\n+ *\n+ *  - Pointer-linked nested structs stp1..stp8: every member is a POINTER to a\n+ *    separately allocated object, so the nesting is followed through the heap:\n+ *        stp1 = { u64 *field0 }\n+ *        stp2 = { u64 *field0, stp1 *field1 }         // { *v, *{v} }\n+ *        stpN = { u64 *field0, stp1 *field1, ... stp(N-1) *field(N-1) }\n+ *    Used by the dynamic-allocation (kmalloc/vmalloc) tests.\n+ *\n+ * Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct to invoke.\n+ */\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/debugfs.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/vmalloc.h\u003e\n+\n+MODULE_LICENSE(\"GPL\");\n+MODULE_DESCRIPTION(\"KCOV dataflow struct field expansion test (flat + nested)\");\n+\n+/* Flat structs: sN has N u64 members. */\n+struct s1 { u64 a; };\n+struct s2 { u64 a; u64 b; };\n+struct s3 { u64 a; u64 b; u64 c; };\n+struct s4 { u64 a; u64 b; u64 c; u64 d; };\n+struct s5 { u64 a; u64 b; u64 c; u64 d; u64 e; };\n+struct s6 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; };\n+struct s7 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; };\n+struct s8 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; u64 h; };\n+\n+/*\n+ * Recursively (value) nested structs: stN = { u64 field0; st1 field1; ...;\n+ * st(N-1) field(N-1); }. Each stN contains every smaller struct by value, so\n+ * the nesting depth grows with N (st8 is eight levels deep along its st7 chain).\n+ */\n+struct st1 { u64 field0; };\n+struct st2 { u64 field0; struct st1 field1; };\n+struct st3 { u64 field0; struct st1 field1; struct st2 field2; };\n+struct st4 {\n+\tu64 field0;\n+\tstruct st1 field1;\n+\tstruct st2 field2;\n+\tstruct st3 field3;\n+};\n+struct st5 {\n+\tu64 field0;\n+\tstruct st1 field1;\n+\tstruct st2 field2;\n+\tstruct st3 field3;\n+\tstruct st4 field4;\n+};\n+struct st6 {\n+\tu64 field0;\n+\tstruct st1 field1;\n+\tstruct st2 field2;\n+\tstruct st3 field3;\n+\tstruct st4 field4;\n+\tstruct st5 field5;\n+};\n+struct st7 {\n+\tu64 field0;\n+\tstruct st1 field1;\n+\tstruct st2 field2;\n+\tstruct st3 field3;\n+\tstruct st4 field4;\n+\tstruct st5 field5;\n+\tstruct st6 field6;\n+};\n+struct st8 {\n+\tu64 field0;\n+\tstruct st1 field1;\n+\tstruct st2 field2;\n+\tstruct st3 field3;\n+\tstruct st4 field4;\n+\tstruct st5 field5;\n+\tstruct st6 field6;\n+\tstruct st7 field7;\n+};\n+\n+/*\n+ * Pointer-linked nested structs: every member is a POINTER to a separately\n+ * allocated object. stpN = { u64 *field0; stp1 *field1; ...; stp(N-1)\n+ * *field(N-1); }. The dynamic-allocation tests build one of these per allocator.\n+ */\n+struct stp1 { u64 *field0; };\n+struct stp2 { u64 *field0; struct stp1 *field1; };\n+struct stp3 { u64 *field0; struct stp1 *field1; struct stp2 *field2; };\n+struct stp4 {\n+\tu64 *field0;\n+\tstruct stp1 *field1;\n+\tstruct stp2 *field2;\n+\tstruct stp3 *field3;\n+};\n+struct stp5 {\n+\tu64 *field0;\n+\tstruct stp1 *field1;\n+\tstruct stp2 *field2;\n+\tstruct stp3 *field3;\n+\tstruct stp4 *field4;\n+};\n+struct stp6 {\n+\tu64 *field0;\n+\tstruct stp1 *field1;\n+\tstruct stp2 *field2;\n+\tstruct stp3 *field3;\n+\tstruct stp4 *field4;\n+\tstruct stp5 *field5;\n+};\n+struct stp7 {\n+\tu64 *field0;\n+\tstruct stp1 *field1;\n+\tstruct stp2 *field2;\n+\tstruct stp3 *field3;\n+\tstruct stp4 *field4;\n+\tstruct stp5 *field5;\n+\tstruct stp6 *field6;\n+};\n+struct stp8 {\n+\tu64 *field0;\n+\tstruct stp1 *field1;\n+\tstruct stp2 *field2;\n+\tstruct stp3 *field3;\n+\tstruct stp4 *field4;\n+\tstruct stp5 *field5;\n+\tstruct stp6 *field6;\n+\tstruct stp7 *field7;\n+};\n+\n+/* Prototypes: sf_N takes N struct pointer arguments (s1*, s2*, ..., sN*) */\n+u64 sf_1(struct s1 *a);\n+u64 sf_2(struct s1 *a, struct s2 *b);\n+u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c);\n+u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);\n+u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e);\n+u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,\n+\t struct s6 *f);\n+u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,\n+\t struct s6 *f, struct s7 *g);\n+u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,\n+\t struct s6 *f, struct s7 *g, struct s8 *h);\n+\n+/* stf_N takes a pointer to the value-nested stN and sums every reachable field0. */\n+u64 stf_1(struct st1 *p);\n+u64 stf_2(struct st2 *p);\n+u64 stf_3(struct st3 *p);\n+u64 stf_4(struct st4 *p);\n+u64 stf_5(struct st5 *p);\n+u64 stf_6(struct st6 *p);\n+u64 stf_7(struct st7 *p);\n+u64 stf_8(struct st8 *p);\n+\n+/* stpf_N follows the pointer-linked stpN and sums every reachable *field0. */\n+u64 stpf_1(struct stp1 *p);\n+u64 stpf_2(struct stp2 *p);\n+u64 stpf_3(struct stp3 *p);\n+u64 stpf_4(struct stp4 *p);\n+u64 stpf_5(struct stp5 *p);\n+u64 stpf_6(struct stp6 *p);\n+u64 stpf_7(struct stp7 *p);\n+u64 stpf_8(struct stp8 *p);\n+\n+noinline u64 sf_1(struct s1 *a) { return a-\u003ea; }\n+EXPORT_SYMBOL(sf_1);\n+\n+noinline u64 sf_2(struct s1 *a, struct s2 *b) { return a-\u003ea + b-\u003eb; }\n+EXPORT_SYMBOL(sf_2);\n+\n+noinline u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec;\n+}\n+EXPORT_SYMBOL(sf_3);\n+\n+noinline u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed;\n+}\n+EXPORT_SYMBOL(sf_4);\n+\n+noinline u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,\n+\t\t  struct s5 *e)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed + e-\u003ee;\n+}\n+EXPORT_SYMBOL(sf_5);\n+\n+noinline u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,\n+\t\t  struct s5 *e, struct s6 *f)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed + e-\u003ee + f-\u003ef;\n+}\n+EXPORT_SYMBOL(sf_6);\n+\n+noinline u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,\n+\t\t  struct s5 *e, struct s6 *f, struct s7 *g)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed + e-\u003ee + f-\u003ef + g-\u003eg;\n+}\n+EXPORT_SYMBOL(sf_7);\n+\n+noinline u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,\n+\t\t  struct s5 *e, struct s6 *f, struct s7 *g, struct s8 *h)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed + e-\u003ee + f-\u003ef + g-\u003eg + h-\u003eh;\n+}\n+EXPORT_SYMBOL(sf_8);\n+\n+/*\n+ * Value-nested functions. Each reads its own field0 and forwards the address of\n+ * every nested member into the matching stf_k, so the whole recursive tower is\n+ * walked and each nesting level is a distinct instrumented struct-pointer arg.\n+ */\n+noinline u64 stf_1(struct st1 *p) { return p-\u003efield0; }\n+EXPORT_SYMBOL(stf_1);\n+\n+noinline u64 stf_2(struct st2 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1);\n+}\n+EXPORT_SYMBOL(stf_2);\n+\n+noinline u64 stf_3(struct st3 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2);\n+}\n+EXPORT_SYMBOL(stf_3);\n+\n+noinline u64 stf_4(struct st4 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2) +\n+\t       stf_3(\u0026p-\u003efield3);\n+}\n+EXPORT_SYMBOL(stf_4);\n+\n+noinline u64 stf_5(struct st5 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2) +\n+\t       stf_3(\u0026p-\u003efield3) + stf_4(\u0026p-\u003efield4);\n+}\n+EXPORT_SYMBOL(stf_5);\n+\n+noinline u64 stf_6(struct st6 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2) +\n+\t       stf_3(\u0026p-\u003efield3) + stf_4(\u0026p-\u003efield4) + stf_5(\u0026p-\u003efield5);\n+}\n+EXPORT_SYMBOL(stf_6);\n+\n+noinline u64 stf_7(struct st7 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2) +\n+\t       stf_3(\u0026p-\u003efield3) + stf_4(\u0026p-\u003efield4) + stf_5(\u0026p-\u003efield5) +\n+\t       stf_6(\u0026p-\u003efield6);\n+}\n+EXPORT_SYMBOL(stf_7);\n+\n+noinline u64 stf_8(struct st8 *p)\n+{\n+\treturn p-\u003efield0 + stf_1(\u0026p-\u003efield1) + stf_2(\u0026p-\u003efield2) +\n+\t       stf_3(\u0026p-\u003efield3) + stf_4(\u0026p-\u003efield4) + stf_5(\u0026p-\u003efield5) +\n+\t       stf_6(\u0026p-\u003efield6) + stf_7(\u0026p-\u003efield7);\n+}\n+EXPORT_SYMBOL(stf_8);\n+\n+/*\n+ * Pointer-linked functions. Each dereferences its own *field0 and forwards each\n+ * (already pointer-typed) nested member into the matching stpf_k, following the\n+ * heap-linked tower.\n+ */\n+noinline u64 stpf_1(struct stp1 *p) { return *p-\u003efield0; }\n+EXPORT_SYMBOL(stpf_1);\n+\n+noinline u64 stpf_2(struct stp2 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1);\n+}\n+EXPORT_SYMBOL(stpf_2);\n+\n+noinline u64 stpf_3(struct stp3 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2);\n+}\n+EXPORT_SYMBOL(stpf_3);\n+\n+noinline u64 stpf_4(struct stp4 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2) +\n+\t       stpf_3(p-\u003efield3);\n+}\n+EXPORT_SYMBOL(stpf_4);\n+\n+noinline u64 stpf_5(struct stp5 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2) +\n+\t       stpf_3(p-\u003efield3) + stpf_4(p-\u003efield4);\n+}\n+EXPORT_SYMBOL(stpf_5);\n+\n+noinline u64 stpf_6(struct stp6 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2) +\n+\t       stpf_3(p-\u003efield3) + stpf_4(p-\u003efield4) + stpf_5(p-\u003efield5);\n+}\n+EXPORT_SYMBOL(stpf_6);\n+\n+noinline u64 stpf_7(struct stp7 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2) +\n+\t       stpf_3(p-\u003efield3) + stpf_4(p-\u003efield4) + stpf_5(p-\u003efield5) +\n+\t       stpf_6(p-\u003efield6);\n+}\n+EXPORT_SYMBOL(stpf_7);\n+\n+noinline u64 stpf_8(struct stp8 *p)\n+{\n+\treturn *p-\u003efield0 + stpf_1(p-\u003efield1) + stpf_2(p-\u003efield2) +\n+\t       stpf_3(p-\u003efield3) + stpf_4(p-\u003efield4) + stpf_5(p-\u003efield5) +\n+\t       stpf_6(p-\u003efield6) + stpf_7(p-\u003efield7);\n+}\n+EXPORT_SYMBOL(stpf_8);\n+\n+u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);\n+u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);\n+struct s4 sf_ret_struct(struct s1 *a, struct s2 *b);\n+\n+/* Pointer forwarding: callee receives pointer and passes it to another func */\n+noinline u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)\n+{\n+\treturn a-\u003ea + b-\u003eb + c-\u003ec + d-\u003ed;\n+}\n+EXPORT_SYMBOL(sf_fwd_inner);\n+\n+noinline u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)\n+{\n+\treturn sf_fwd_inner(a, b, c, d);\n+}\n+EXPORT_SYMBOL(sf_fwd);\n+\n+/* Struct return value */\n+noinline struct s4 sf_ret_struct(struct s1 *a, struct s2 *b)\n+{\n+\tstruct s4 ret = { .a = a-\u003ea, .b = b-\u003ea, .c = b-\u003eb, .d = a-\u003ea + b-\u003eb };\n+\n+\treturn ret;\n+}\n+EXPORT_SYMBOL(sf_ret_struct);\n+\n+/* Allocator shims so run_stp8() can build the pointer tree with either API. */\n+static void *t_kmalloc(size_t n) { return kmalloc(n, GFP_KERNEL); }\n+static void *t_vmalloc(size_t n) { return vmalloc(n); }\n+static void t_kfree(void *p) { kfree(p); }\n+static void t_vfree(void *p) { vfree(p); }\n+\n+/*\n+ * Build the pointer-linked stp8 tower with @alloc (each node separately\n+ * allocated), run stpf_8() over it, then free every node with @fr. Sub-nodes\n+ * are shared (a DAG); each unique allocation is freed exactly once.\n+ */\n+static u64 run_stp8(void *(*alloc)(size_t), void (*fr)(void *))\n+{\n+\tu64 ret = 0;\n+\tu64 *l1 = alloc(sizeof(u64));\n+\tu64 *l2 = alloc(sizeof(u64));\n+\tu64 *l3 = alloc(sizeof(u64));\n+\tu64 *l4 = alloc(sizeof(u64));\n+\tu64 *l5 = alloc(sizeof(u64));\n+\tu64 *l6 = alloc(sizeof(u64));\n+\tu64 *l7 = alloc(sizeof(u64));\n+\tu64 *l8 = alloc(sizeof(u64));\n+\tstruct stp1 *p1 = alloc(sizeof(*p1));\n+\tstruct stp2 *p2 = alloc(sizeof(*p2));\n+\tstruct stp3 *p3 = alloc(sizeof(*p3));\n+\tstruct stp4 *p4 = alloc(sizeof(*p4));\n+\tstruct stp5 *p5 = alloc(sizeof(*p5));\n+\tstruct stp6 *p6 = alloc(sizeof(*p6));\n+\tstruct stp7 *p7 = alloc(sizeof(*p7));\n+\tstruct stp8 *p8 = alloc(sizeof(*p8));\n+\n+\tif (l1 \u0026\u0026 l2 \u0026\u0026 l3 \u0026\u0026 l4 \u0026\u0026 l5 \u0026\u0026 l6 \u0026\u0026 l7 \u0026\u0026 l8 \u0026\u0026\n+\t    p1 \u0026\u0026 p2 \u0026\u0026 p3 \u0026\u0026 p4 \u0026\u0026 p5 \u0026\u0026 p6 \u0026\u0026 p7 \u0026\u0026 p8) {\n+\t\t*l1 = 0x11; *l2 = 0x22; *l3 = 0x33; *l4 = 0x44;\n+\t\t*l5 = 0x55; *l6 = 0x66; *l7 = 0x77; *l8 = 0x88;\n+\n+\t\tp1-\u003efield0 = l1;\n+\t\tp2-\u003efield0 = l2; p2-\u003efield1 = p1;\n+\t\tp3-\u003efield0 = l3; p3-\u003efield1 = p1; p3-\u003efield2 = p2;\n+\t\tp4-\u003efield0 = l4; p4-\u003efield1 = p1; p4-\u003efield2 = p2;\n+\t\tp4-\u003efield3 = p3;\n+\t\tp5-\u003efield0 = l5; p5-\u003efield1 = p1; p5-\u003efield2 = p2;\n+\t\tp5-\u003efield3 = p3; p5-\u003efield4 = p4;\n+\t\tp6-\u003efield0 = l6; p6-\u003efield1 = p1; p6-\u003efield2 = p2;\n+\t\tp6-\u003efield3 = p3; p6-\u003efield4 = p4; p6-\u003efield5 = p5;\n+\t\tp7-\u003efield0 = l7; p7-\u003efield1 = p1; p7-\u003efield2 = p2;\n+\t\tp7-\u003efield3 = p3; p7-\u003efield4 = p4; p7-\u003efield5 = p5;\n+\t\tp7-\u003efield6 = p6;\n+\t\tp8-\u003efield0 = l8; p8-\u003efield1 = p1; p8-\u003efield2 = p2;\n+\t\tp8-\u003efield3 = p3; p8-\u003efield4 = p4; p8-\u003efield5 = p5;\n+\t\tp8-\u003efield6 = p6; p8-\u003efield7 = p7;\n+\n+\t\tret = stpf_8(p8);\n+\t}\n+\n+\tfr(p8); fr(p7); fr(p6); fr(p5); fr(p4); fr(p3); fr(p2); fr(p1);\n+\tfr(l8); fr(l7); fr(l6); fr(l5); fr(l4); fr(l3); fr(l2); fr(l1);\n+\treturn ret;\n+}\n+\n+static struct dentry *test_dir;\n+\n+static ssize_t trigger_write(struct file *f, const char __user *buf,\n+\t\t\t     size_t count, loff_t *ppos)\n+{\n+\tstruct s1 v1 = { .a = 0x11 };\n+\tstruct s2 v2 = { .a = 0x11, .b = 0x22 };\n+\tstruct s3 v3 = { .a = 0x11, .b = 0x22, .c = 0x33 };\n+\tstruct s4 v4 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44 };\n+\tstruct s5 v5 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,\n+\t\t\t .e = 0x55 };\n+\tstruct s6 v6 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,\n+\t\t\t .e = 0x55, .f = 0x66 };\n+\tstruct s7 v7 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,\n+\t\t\t .e = 0x55, .f = 0x66, .g = 0x77 };\n+\tstruct s8 v8 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,\n+\t\t\t .e = 0x55, .f = 0x66, .g = 0x77, .h = 0x88 };\n+\n+\t/* Recursively (value) nested values: each embeds all the smaller ones. */\n+\tstruct st1 t1 = { .field0 = 0x11 };\n+\tstruct st2 t2 = { .field0 = 0x22, .field1 = t1 };\n+\tstruct st3 t3 = { .field0 = 0x33, .field1 = t1, .field2 = t2 };\n+\tstruct st4 t4 = { .field0 = 0x44, .field1 = t1, .field2 = t2,\n+\t\t\t  .field3 = t3 };\n+\tstruct st5 t5 = { .field0 = 0x55, .field1 = t1, .field2 = t2,\n+\t\t\t  .field3 = t3, .field4 = t4 };\n+\tstruct st6 t6 = { .field0 = 0x66, .field1 = t1, .field2 = t2,\n+\t\t\t  .field3 = t3, .field4 = t4, .field5 = t5 };\n+\tstruct st7 t7 = { .field0 = 0x77, .field1 = t1, .field2 = t2,\n+\t\t\t  .field3 = t3, .field4 = t4, .field5 = t5,\n+\t\t\t  .field6 = t6 };\n+\tu64 sum = 0;\n+\n+\t/* Flat struct tests: sf_N takes N struct pointer args */\n+\tsum += sf_1(\u0026v1);\n+\tsum += sf_2(\u0026v1, \u0026v2);\n+\tsum += sf_3(\u0026v1, \u0026v2, \u0026v3);\n+\tsum += sf_4(\u0026v1, \u0026v2, \u0026v3, \u0026v4);\n+\tsum += sf_5(\u0026v1, \u0026v2, \u0026v3, \u0026v4, \u0026v5);\n+\tsum += sf_6(\u0026v1, \u0026v2, \u0026v3, \u0026v4, \u0026v5, \u0026v6);\n+\tsum += sf_7(\u0026v1, \u0026v2, \u0026v3, \u0026v4, \u0026v5, \u0026v6, \u0026v7);\n+\tsum += sf_8(\u0026v1, \u0026v2, \u0026v3, \u0026v4, \u0026v5, \u0026v6, \u0026v7, \u0026v8);\n+\n+\t/* Value-nested struct tests (on-stack) */\n+\tsum += stf_1(\u0026t1);\n+\tsum += stf_2(\u0026t2);\n+\tsum += stf_3(\u0026t3);\n+\tsum += stf_4(\u0026t4);\n+\tsum += stf_5(\u0026t5);\n+\tsum += stf_6(\u0026t6);\n+\tsum += stf_7(\u0026t7);\n+\t/*\n+\t * st8 is 1 KiB; keeping it on the stack alongside t1..t7 blows the 2048-byte\n+\t * frame limit (-Wframe-larger-than). Build it on the heap (member-wise, so no\n+\t * 1 KiB compound-literal temporary lands on the stack either).\n+\t */\n+\t{\n+\t\tstruct st8 *t8 = kmalloc(sizeof(*t8), GFP_KERNEL);\n+\n+\t\tif (t8) {\n+\t\t\tt8-\u003efield0 = 0x88;\n+\t\t\tt8-\u003efield1 = t1;\n+\t\t\tt8-\u003efield2 = t2;\n+\t\t\tt8-\u003efield3 = t3;\n+\t\t\tt8-\u003efield4 = t4;\n+\t\t\tt8-\u003efield5 = t5;\n+\t\t\tt8-\u003efield6 = t6;\n+\t\t\tt8-\u003efield7 = t7;\n+\t\t\tsum += stf_8(t8);\n+\t\t\tkfree(t8);\n+\t\t}\n+\t}\n+\n+\t/* Dynamic allocation: pointer-linked stp8, each node separately alloc'd */\n+\tsum += run_stp8(t_kmalloc, t_kfree);\t/* heap/slab */\n+\tsum += run_stp8(t_vmalloc, t_vfree);\t/* vmalloc address space */\n+\n+\t/* Pointer forwarding: sf_fwd receives pointers and forwards to inner */\n+\tsum += sf_fwd(\u0026v1, \u0026v2, \u0026v3, \u0026v4);\n+\n+\t/* Struct return value */\n+\t{\n+\t\tstruct s4 ret = sf_ret_struct(\u0026v1, \u0026v2);\n+\n+\t\tsum += ret.a + ret.b + ret.c + ret.d;\n+\t}\n+\n+\t/* Keep every call above from being optimised away (sum is otherwise dead). */\n+\tOPTIMIZER_HIDE_VAR(sum);\n+\treturn count;\n+}\n+\n+static const struct file_operations trigger_fops = {\n+\t.write = trigger_write,\n+};\n+\n+static int __init eight_struct_args_init(void)\n+{\n+\ttest_dir = debugfs_create_dir(\"kcov_dataflow_test\", NULL);\n+\tdebugfs_create_file(\"trigger_struct\", 0200, test_dir, NULL,\n+\t\t\t    \u0026trigger_fops);\n+\treturn 0;\n+}\n+\n+static void __exit eight_struct_args_exit(void)\n+{\n+\tdebugfs_remove_recursive(test_dir);\n+}\n+\n+module_init(eight_struct_args_init);\n+module_exit(eight_struct_args_exit);\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile\nnew file mode 100644\nindex 0000000000000..3017a24774051\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile\n@@ -0,0 +1,3 @@\n+# SPDX-License-Identifier: GPL-2.0\n+obj-m := eight_struct_args_rust.o\n+KCOV_DATAFLOW_eight_struct_args_rust.o := y\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst\nnew file mode 100644\nindex 0000000000000..06e8f8070f6c2\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst\n@@ -0,0 +1,11 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: eight_struct_args_rust\n+===============================================\n+\n+Rust equivalent of eight_struct_args_c (rsf_*, rstf_*, rstpf_* with\n+``#[no_mangle]``), built only with CONFIG_RUST=y. Opted in with\n+``KCOV_DATAFLOW_eight_struct_args_rust.o := y``::\n+\n+  ./test_modules.py -t eight_struct_args_rust\n+  ./trigger-view.py eight_struct_args_rust --raw\ndiff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs\nnew file mode 100644\nindex 0000000000000..e5cc3cb87591e\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs\n@@ -0,0 +1,646 @@\n+// SPDX-License-Identifier: GPL-2.0\n+//! Verify kcov_dataflow captures struct pointer arguments with automatic\n+//! field expansion for Rust #[repr(C)] structs.\n+//!\n+//! Rust equivalent of eight_struct_args_c. Two families are exercised:\n+//!   - Flat structs S1..S8 (1-8 u64 members) via rsf_N.\n+//!   - Recursively (value) nested structs St1..St8, where StN embeds every\n+//!     smaller struct by value:\n+//!         St1 = { field0 }\n+//!         St2 = { field0, field1: St1 }              // { v, {v} }\n+//!         StN = { field0, field1: St1, ..., field(N-1): St(N-1) }\n+//!     so St8 is eight levels deep along its St7 chain. Each rstf_N reads its\n+//!     own field0 and forwards each nested member's address into rstf_k.\n+//!   - Pointer-linked nested structs Stp1..Stp8, where every member is a raw\n+//!     pointer to a separately allocated object:\n+//!         Stp1 = { field0: *const u64 }\n+//!         StpN = { field0: *const u64, field1: *const Stp1, ... }\n+//!     The heap (KBox) test builds this tower and follows it via rstpf_N.\n+//!\n+//! Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct_rust to invoke.\n+\n+#![allow(missing_docs)]\n+\n+use kernel::prelude::*;\n+use kernel::alloc::KBox;\n+use kernel::c_str;\n+\n+module !{\n+\ttype:EightStructArgsRust,\n+\tname: \"eight_struct_args_rust\",\n+\tauthors: [\"kcov-dataflow\"],\n+\tdescription: \"Struct field expansion test for kcov_dataflow (Rust)\",\n+\tlicense: \"GPL\",\n+}\n+#[repr(C)]\n+pub struct S1 {\n+\tpub a : u64\n+}\n+#[repr(C)]\n+pub struct S2 {\n+\tpub a : u64, pub b : u64\n+}\n+#[repr(C)]\n+pub struct S3 {\n+\tpub a : u64, pub b : u64, pub c : u64\n+}\n+#[repr(C)]\n+pub struct S4 {\n+\tpub a : u64, pub b : u64, pub c : u64, pub d : u64\n+}\n+#[repr(C)]\n+pub struct S5 {\n+\tpub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64\n+}\n+#[repr(C)]\n+pub struct S6 {\n+\tpub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,\n+\t\tpub f : u64\n+}\n+#[repr(C)]\n+pub struct S7 {\n+\tpub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,\n+\t\tpub f : u64, pub g : u64\n+}\n+#[repr(C)]\n+pub struct S8 {\n+\tpub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,\n+\t\tpub f : u64, pub g : u64, pub h : u64\n+}\n+// Recursively nested: StN = { field0, field1: St1, ..., field(N-1): St(N-1) }.\n+// Copy so a smaller value can be embedded into every larger one.\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St1 {\n+\tpub field0 : u64\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St2 {\n+\tpub field0 : u64, pub field1 : St1\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St3 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St4 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St5 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,\n+\t\tpub field4 : St4\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St6 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,\n+\t\tpub field4 : St4, pub field5 : St5\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St7 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,\n+\t\tpub field4 : St4, pub field5 : St5, pub field6 : St6\n+}\n+#[repr(C)]\n+#[derive(Clone, Copy)]\n+pub struct St8 {\n+\tpub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,\n+\t\tpub field4 : St4, pub field5 : St5, pub field6 : St6,\n+\t\tpub field7 : St7\n+}\n+// Pointer-linked nested: every member is a raw pointer to a separately\n+// allocated object. StpN = { field0: *const u64, field1: *const Stp1, ... }.\n+#[repr(C)]\n+pub struct Stp1 {\n+\tpub field0 : *const u64\n+}\n+#[repr(C)]\n+pub struct Stp2 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1\n+}\n+#[repr(C)]\n+pub struct Stp3 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2\n+}\n+#[repr(C)]\n+pub struct Stp4 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2, pub field3 : *const Stp3\n+}\n+#[repr(C)]\n+pub struct Stp5 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2, pub field3 : *const Stp3,\n+\t\tpub field4 : *const Stp4\n+}\n+#[repr(C)]\n+pub struct Stp6 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2, pub field3 : *const Stp3,\n+\t\tpub field4 : *const Stp4, pub field5 : *const Stp5\n+}\n+#[repr(C)]\n+pub struct Stp7 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2, pub field3 : *const Stp3,\n+\t\tpub field4 : *const Stp4, pub field5 : *const Stp5,\n+\t\tpub field6 : *const Stp6\n+}\n+#[repr(C)]\n+pub struct Stp8 {\n+\tpub field0 : *const u64, pub field1 : *const Stp1,\n+\t\tpub field2 : *const Stp2, pub field3 : *const Stp3,\n+\t\tpub field4 : *const Stp4, pub field5 : *const Stp5,\n+\t\tpub field6 : *const Stp6, pub field7 : *const Stp7\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_1(a : *const S1) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*a).a\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_2(a : *const S1, b : *const S2) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*a).a + (*b).b\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_4(a : *const S1, b : *const S2, c : *const S3,\n+\t\t\td : *const S4) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*a).a + (*b).b + (*c).c + (*d).d\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_8(a : *const S1, b : *const S2, c : *const S3,\n+\t\t\td : *const S4, e : *const S5, f : *const S6,\n+\t\t\tg : *const S7, h : *const S8) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*a).a + (*b).b + (*c).c + (*d).d + (*e).e + (*f).f + (*g).g +\n+\t\t\t(*h).h\n+\t}\n+}\n+\n+// Recursively nested: each reads its own field0 and forwards every nested\n+// member's address into the matching rstf_k, walking the whole tower.\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_1(p : *const St1) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_2(p : *const St2) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_3(p : *const St3) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_4(p : *const St4) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2) +\n+\t\t\trstf_3(\u0026(*p).field3)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_5(p : *const St5) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2) +\n+\t\t\trstf_3(\u0026(*p).field3) + rstf_4(\u0026(*p).field4)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_6(p : *const St6) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2) +\n+\t\t\trstf_3(\u0026(*p).field3) + rstf_4(\u0026(*p).field4) +\n+\t\t\trstf_5(\u0026(*p).field5)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_7(p : *const St7) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2) +\n+\t\t\trstf_3(\u0026(*p).field3) + rstf_4(\u0026(*p).field4) +\n+\t\t\trstf_5(\u0026(*p).field5) + rstf_6(\u0026(*p).field6)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstf_8(p : *const St8) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*p).field0 + rstf_1(\u0026(*p).field1) + rstf_2(\u0026(*p).field2) +\n+\t\t\trstf_3(\u0026(*p).field3) + rstf_4(\u0026(*p).field4) +\n+\t\t\trstf_5(\u0026(*p).field5) + rstf_6(\u0026(*p).field6) +\n+\t\t\trstf_7(\u0026(*p).field7)\n+\t}\n+}\n+\n+// Pointer-linked: each dereferences its own *field0 and forwards each\n+// (already pointer-typed) nested member into the matching rstpf_k.\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_1(p : *const Stp1) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_2(p : *const Stp2) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_3(p : *const Stp3) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_4(p : *const Stp4) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +\n+\t\t\trstpf_3((*p).field3)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_5(p : *const Stp5) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +\n+\t\t\trstpf_3((*p).field3) + rstpf_4((*p).field4)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_6(p : *const Stp6) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +\n+\t\t\trstpf_3((*p).field3) + rstpf_4((*p).field4) +\n+\t\t\trstpf_5((*p).field5)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_7(p : *const Stp7) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +\n+\t\t\trstpf_3((*p).field3) + rstpf_4((*p).field4) +\n+\t\t\trstpf_5((*p).field5) + rstpf_6((*p).field6)\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rstpf_8(p : *const Stp8) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +\n+\t\t\trstpf_3((*p).field3) + rstpf_4((*p).field4) +\n+\t\t\trstpf_5((*p).field5) + rstpf_6((*p).field6) +\n+\t\t\trstpf_7((*p).field7)\n+\t}\n+}\n+\n+// Build the pointer-linked Stp8 tower with KBox (each node its own allocation),\n+// run rstpf_8 over it, and return the sum. The KBoxes own the storage and hold\n+// raw pointers into their siblings; everything is freed when they drop at the\n+// end of this function. `?` frees any already-allocated KBoxes on OOM.\n+fn build_and_run_stp8() -\u003e Result\u003cu64\u003e\n+{\n+\tlet l1 = KBox::new (0x11u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l2 = KBox::new (0x22u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l3 = KBox::new (0x33u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l4 = KBox::new (0x44u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l5 = KBox::new (0x55u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l6 = KBox::new (0x66u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l7 = KBox::new (0x77u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\tlet l8 = KBox::new (0x88u64, kernel::alloc::flags::GFP_KERNEL) ? ;\n+\n+\tlet p1 = KBox::new (Stp1{ field0: \u0026*l1 },\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p2 = KBox::new (Stp2{ field0: \u0026*l2, field1: \u0026*p1 },\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p3 = KBox::new (Stp3{ field0: \u0026*l3, field1: \u0026*p1, field2: \u0026*p2 },\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p4 = KBox::new (\n+\t\tStp4{ field0: \u0026*l4, field1: \u0026*p1, field2: \u0026*p2, field3: \u0026*p3 },\n+\t\tkernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p5 = KBox::new (Stp5{\n+\t\tfield0: \u0026*l5,\n+\t\tfield1: \u0026*p1,\n+\t\tfield2: \u0026*p2,\n+\t\tfield3: \u0026*p3,\n+\t\tfield4: \u0026*p4\n+\t},\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p6 = KBox::new (Stp6{\n+\t\tfield0: \u0026*l6,\n+\t\tfield1: \u0026*p1,\n+\t\tfield2: \u0026*p2,\n+\t\tfield3: \u0026*p3,\n+\t\tfield4: \u0026*p4,\n+\t\tfield5: \u0026*p5\n+\t},\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p7 = KBox::new (Stp7{\n+\t\tfield0: \u0026*l7,\n+\t\tfield1: \u0026*p1,\n+\t\tfield2: \u0026*p2,\n+\t\tfield3: \u0026*p3,\n+\t\tfield4: \u0026*p4,\n+\t\tfield5: \u0026*p5,\n+\t\tfield6: \u0026*p6\n+\t},\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\tlet p8 = KBox::new (Stp8{\n+\t\tfield0: \u0026*l8,\n+\t\tfield1: \u0026*p1,\n+\t\tfield2: \u0026*p2,\n+\t\tfield3: \u0026*p3,\n+\t\tfield4: \u0026*p4,\n+\t\tfield5: \u0026*p5,\n+\t\tfield6: \u0026*p6,\n+\t\tfield7: \u0026*p7\n+\t},\n+\t\t\t    kernel::alloc::flags::GFP_KERNEL) ?\n+\t\t;\n+\n+\tOk(rstpf_8(\u0026*p8))\n+}\n+\n+/* Pointer forwarding: receives pointers and passes to inner */\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_fwd_inner(a : *const S1, b : *const S2, c : *const S3,\n+\t\t\t\td : *const S4) -\u003e u64\n+{\n+\tunsafe\n+\t{\n+\t\t(*a).a + (*b).b + (*c).c + (*d).d\n+\t}\n+}\n+\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_fwd(a : *const S1, b : *const S2, c : *const S3,\n+\t\t\t  d : *const S4) -\u003e u64{ rsf_fwd_inner(a, b, c, d) }\n+\n+/* Struct return value */\n+#[no_mangle]\n+#[inline(never)]\n+pub extern \"C\" fn rsf_ret_struct(a : *const S1, b : *const S2)\n+\t-\u003eS4\n+{\n+\tunsafe\n+\t{\n+\t\tS4\n+\t\t{\n+a:\n+\t\t\t(*a).a, b : (*b).a, c : (*b).b, d : (*a).a + (*b).b\n+\t\t}\n+\t}\n+}\n+\n+unsafe extern \"C\" fn write_handler(_file : *mut kernel::bindings::file,\n+\t\t\t\t   _buf : *const core::ffi::c_char,\n+\t\t\t\t   count : usize,\n+\t\t\t\t   _ppos : *mut kernel::bindings::loff_t, )\n+\t-\u003e kernel::ffi::c_long\n+{\n+\tlet v1 = S1{ a: 0x11 };\n+\tlet v2 = S2{ a: 0x11, b: 0x22 };\n+\tlet v3 = S3{ a: 0x11, b: 0x22, c: 0x33 };\n+\tlet v4 = S4{ a: 0x11, b: 0x22, c: 0x33, d: 0x44 };\n+\tlet v5 = S5{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55 };\n+\tlet v6 = S6{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66 };\n+\tlet v7 =\n+\tS7{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66, g: 0x77 };\n+\tlet v8 = S8{\n+\t\ta: 0x11,\n+\t\tb: 0x22,\n+\t\tc: 0x33,\n+\t\td: 0x44,\n+\t\te: 0x55,\n+\t\tf: 0x66,\n+\t\tg: 0x77,\n+\t\th: 0x88\n+\t};\n+\n+\t// Recursively nested values: each embeds all the smaller ones (Copy).\n+\tlet t1 = St1{ field0: 0x11 };\n+\tlet t2 = St2{ field0: 0x22, field1: t1 };\n+\tlet t3 = St3{ field0: 0x33, field1: t1, field2: t2 };\n+\tlet t4 = St4{ field0: 0x44, field1: t1, field2: t2, field3: t3 };\n+\tlet t5 =\n+\tSt5{ field0: 0x55, field1: t1, field2: t2, field3: t3, field4: t4 };\n+\tlet t6 = St6{\n+\t\tfield0: 0x66,\n+\t\tfield1: t1,\n+\t\tfield2: t2,\n+\t\tfield3: t3,\n+\t\tfield4: t4,\n+\t\tfield5: t5\n+\t};\n+\tlet t7 = St7{\n+\t\tfield0: 0x77,\n+\t\tfield1: t1,\n+\t\tfield2: t2,\n+\t\tfield3: t3,\n+\t\tfield4: t4,\n+\t\tfield5: t5,\n+\t\tfield6: t6\n+\t};\n+\tlet t8 = St8{\n+\t\tfield0: 0x88,\n+\t\tfield1: t1,\n+\t\tfield2: t2,\n+\t\tfield3: t3,\n+\t\tfield4: t4,\n+\t\tfield5: t5,\n+\t\tfield6: t6,\n+\t\tfield7: t7\n+\t};\n+\n+\tlet mut sum : u64 = 0;\n+\tsum = sum.wrapping_add(rsf_1(\u0026v1 as *const S1));\n+\tsum = sum.wrapping_add(rsf_2(\u0026v1 as *const S1, \u0026v2 as *const S2));\n+\tsum = sum.wrapping_add(rsf_4(\u0026v1 as *const S1, \u0026v2 as *const S2,\n+\t\t\t\t     \u0026v3 as *const S3, \u0026v4 as *const S4));\n+\tsum = sum.wrapping_add(rsf_8(\u0026v1 as *const S1, \u0026v2 as *const S2,\n+\t\t\t\t     \u0026v3 as *const S3, \u0026v4 as *const S4,\n+\t\t\t\t     \u0026v5 as *const S5, \u0026v6 as *const S6,\n+\t\t\t\t     \u0026v7 as *const S7, \u0026v8 as *const S8));\n+\n+\t// Recursively nested struct tests\n+\tsum = sum.wrapping_add(rstf_1(\u0026t1 as *const St1));\n+\tsum = sum.wrapping_add(rstf_2(\u0026t2 as *const St2));\n+\tsum = sum.wrapping_add(rstf_3(\u0026t3 as *const St3));\n+\tsum = sum.wrapping_add(rstf_4(\u0026t4 as *const St4));\n+\tsum = sum.wrapping_add(rstf_5(\u0026t5 as *const St5));\n+\tsum = sum.wrapping_add(rstf_6(\u0026t6 as *const St6));\n+\tsum = sum.wrapping_add(rstf_7(\u0026t7 as *const St7));\n+\tsum = sum.wrapping_add(rstf_8(\u0026t8 as *const St8));\n+\n+\t// Pointer forwarding: rsf_fwd receives and passes to rsf_fwd_inner\n+\tsum = sum.wrapping_add(rsf_fwd(\u0026v1 as *const S1, \u0026v2 as *const S2,\n+\t\t\t\t       \u0026v3 as *const S3, \u0026v4 as *const S4));\n+\n+\t// Struct return value\n+\tlet ret = rsf_ret_struct(\u0026v1 as *const S1, \u0026v2 as *const S2);\n+\tsum = sum.wrapping_add(ret.a + ret.b + ret.c + ret.d);\n+\n+\t// Dynamic allocation: pointer-linked Stp8 tower (each node its own KBox)\n+\tif let\n+\t\tOk(s) = build_and_run_stp8()\n+\t\t{\n+\t\t\tsum = sum.wrapping_add(s);\n+\t\t}\n+\n+\tcore::hint::black_box(sum);\n+\tcount as kernel::ffi::c_long\n+}\n+\n+#[repr(transparent)]\n+struct SyncFops(kernel::bindings::file_operations);\n+unsafe impl Sync for SyncFops\n+{\n+}\n+\n+static FOPS : SyncFops = SyncFops(kernel::bindings::file_operations{\n+\twrite: Some(unsafe{ core::mem::transmute(write_handler as *const()) }),\n+\t..unsafe{ core::mem::zeroed() }\n+});\n+\n+struct EightStructArgsRust {\n+\tdir : *mut kernel::bindings::dentry,\n+}\n+\n+impl kernel::Module for EightStructArgsRust\n+{\n+    fn init(_module: \u0026'static ThisModule) -\u003e Result\u003cSelf\u003e {\n+        let dir = unsafe {\n+            kernel::bindings::debugfs_create_dir(\n+                c_str!(\"kcov_dataflow_test\").as_char_ptr(),\n+                core::ptr::null_mut(),\n+            )\n+        };\n+        unsafe {\n+            kernel::bindings::debugfs_create_file_unsafe(\n+                c_str!(\"trigger_struct_rust\").as_char_ptr(),\n+                0o222,\n+                dir,\n+                core::ptr::null_mut(),\n+                \u0026FOPS.0,\n+            )\n+        };\n+        Ok(Self { dir })\n+}\n+}\n+\n+impl Drop for EightStructArgsRust\n+{\n+\tfn drop(\u0026mut self)\n+\t{\n+\t\tunsafe{ kernel::bindings::debugfs_remove(self.dir) };\n+\t}\n+}\n+\n+unsafe impl Send for EightStructArgsRust\n+{\n+}\n+unsafe impl Sync for EightStructArgsRust\n+{\n+}\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile\nnew file mode 100644\nindex 0000000000000..d2a0261070b1c\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile\n@@ -0,0 +1,3 @@\n+# SPDX-License-Identifier: GPL-2.0\n+obj-m := rust_ffi_contract.o\n+KCOV_DATAFLOW_rust_ffi_contract.o := y\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst\nnew file mode 100644\nindex 0000000000000..291621fa799cd\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst\n@@ -0,0 +1,13 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: rust_ffi_contract\n+==========================================\n+\n+FFI contract violation detection: ffi_alloc_buf() returns 0 but leaves\n+alloc-\u003ebuffer NULL, and ffi_check_result() receives that NULL. The test\n+checks the expanded ``struct ffi_alloc`` at both boundaries, the scalar\n+arguments (256, 16, 1), the 0 return and the -EFAULT from the checker.\n+Opted in with ``KCOV_DATAFLOW_rust_ffi_contract.o := y``::\n+\n+  ./test_modules.py -t rust_ffi_contract\n+  ./trigger-view.py rust_ffi_contract -C 8\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c\nnew file mode 100644\nindex 0000000000000..071bd25dfec11\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c\n@@ -0,0 +1,125 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * rust_ffi_contract.c - Demonstrates kcov_dataflow detecting an FFI\n+ * contract violation at a function boundary.\n+ *\n+ * The pattern: caller passes a struct pointer to callee. Callee's\n+ * contract says \"returns 0 implies out-\u003ebuffer is valid\". A bug in\n+ * the async path returns 0 but leaves buffer=NULL.\n+ *\n+ * kcov_dataflow captures:\n+ *   [ENTRY] ffi_alloc_buf(alloc={.buffer=NULL, .data_size=0}, 256, 16, 1)\n+ *   [RET]   ffi_alloc_buf() = 0\n+ *   [ENTRY] ffi_check_result(alloc={.buffer=NULL, .data_size=0x110, ...})\n+ *                             ^ proves contract violated\n+ *   [RET]   ffi_check_result() = -EFAULT\n+ *\n+ * Write to /sys/kernel/debug/kcov_dataflow_test/rust_ffi_trigger to run.\n+ */\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/debugfs.h\u003e\n+#include \u003clinux/slab.h\u003e\n+\n+MODULE_LICENSE(\"GPL\");\n+MODULE_DESCRIPTION(\"FFI contract violation detection via kcov_dataflow\");\n+\n+struct ffi_alloc {\n+\tvoid *buffer;\n+\tu64 data_size;\n+\tu32 free_async;\n+\tu32 flags;\n+};\n+\n+/* Prototypes */\n+int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,\n+\t\t  u64 offsets_size, int is_async);\n+int ffi_check_result(struct ffi_alloc *alloc);\n+\n+/*\n+ * Callee with contract: returns 0 implies alloc-\u003ebuffer is valid.\n+ * BUG: async path with free_async==0 returns 0 but buffer stays NULL.\n+ */\n+noinline int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,\n+\t\t\t   u64 offsets_size, int is_async)\n+{\n+\t/*\n+\t * data_size + offsets_size is used on every path so that the compiler\n+\t * keeps offsets_size alive (an unused parameter is dropped at -O2 and\n+\t * callers then pass poison, leaving nothing to trace).\n+\t */\n+\tif (!is_async) {\n+\t\talloc-\u003ebuffer = kmalloc(data_size + offsets_size, GFP_KERNEL);\n+\t\tif (!alloc-\u003ebuffer)\n+\t\t\treturn -ENOMEM;\n+\t\treturn 0;\n+\t}\n+\t/* BUG: returns success but buffer is NULL when pool empty */\n+\tif (alloc-\u003efree_async == 0) {\n+\t\talloc-\u003ebuffer = NULL;\n+\t\talloc-\u003edata_size = data_size + offsets_size;\n+\t\treturn 0; /* contract violation */\n+\t}\n+\talloc-\u003ebuffer = kmalloc(data_size + offsets_size, GFP_KERNEL);\n+\talloc-\u003efree_async--;\n+\treturn 0;\n+}\n+EXPORT_SYMBOL(ffi_alloc_buf);\n+\n+/* Caller that trusts the contract */\n+noinline int ffi_check_result(struct ffi_alloc *alloc)\n+{\n+\tif (!alloc-\u003ebuffer) {\n+\t\tpr_err(\"ffi_contract: VIOLATION detected - buffer is NULL after success\\n\");\n+\t\treturn -EFAULT;\n+\t}\n+\tkfree(alloc-\u003ebuffer);\n+\treturn 0;\n+}\n+EXPORT_SYMBOL(ffi_check_result);\n+\n+static struct dentry *test_dir;\n+\n+static ssize_t rust_ffi_trigger_write(struct file *f, const char __user *buf,\n+\t\t\t\t size_t count, loff_t *ppos)\n+{\n+\tstruct ffi_alloc alloc = { .buffer = NULL, .data_size = 0,\n+\t\t\t\t   .free_async = 0, .flags = 0 };\n+\tint ret;\n+\n+\t/*\n+\t * Keep the initializer: the callee provably writes alloc-\u003ebuffer before\n+\t * reading it, so without the barrier the compiler drops the NULL store\n+\t * and the ENTRY record would show stack garbage instead of NULL.\n+\t */\n+\tbarrier_data(\u0026alloc);\n+\n+\t/* Trigger the bug: is_async=1, free_async=0 */\n+\tret = ffi_alloc_buf(\u0026alloc, 256, 16, 1);\n+\tpr_info(\"ffi_contract: ffi_alloc_buf returned %d, buffer=%p\\n\",\n+\t\tret, alloc.buffer);\n+\n+\tif (ret == 0)\n+\t\tffi_check_result(\u0026alloc);\n+\n+\treturn count;\n+}\n+\n+static const struct file_operations rust_ffi_trigger_fops = {\n+\t.write = rust_ffi_trigger_write,\n+};\n+\n+static int __init ffi_contract_init(void)\n+{\n+\ttest_dir = debugfs_create_dir(\"kcov_dataflow_test\", NULL);\n+\tdebugfs_create_file(\"rust_ffi_trigger\", 0200, test_dir, NULL,\n+\t\t\t    \u0026rust_ffi_trigger_fops);\n+\treturn 0;\n+}\n+\n+static void __exit ffi_contract_exit(void)\n+{\n+\tdebugfs_remove_recursive(test_dir);\n+}\n+\n+module_init(ffi_contract_init);\n+module_exit(ffi_contract_exit);\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile\nnew file mode 100644\nindex 0000000000000..cb7392a50b1a9\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile\n@@ -0,0 +1,3 @@\n+# SPDX-License-Identifier: GPL-2.0\n+obj-m := rust_kworker_remote.o\n+KCOV_DATAFLOW_rust_kworker_remote.o := y\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst\nnew file mode 100644\nindex 0000000000000..aff597ab67aea\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst\n@@ -0,0 +1,13 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: rust_kworker_remote\n+============================================\n+\n+Rust module testing kcov_df_remote_start()/kcov_df_remote_stop() from\n+kworker context: the trigger queues a work item on system_wq whose three\n+phases (populate/update/drain of a CompositeStore of RBTrees) run with\n+remote capture on handle 1, which the runner publishes with\n+KCOV_DF_REMOTE_ENABLE. Built only with CONFIG_RUST=y::\n+\n+  ./test_modules.py -t rust_kworker_remote\n+  ./trigger-view.py rust_kworker_remote --remote\ndiff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs\nnew file mode 100644\nindex 0000000000000..65c5722c383cc\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs\n@@ -0,0 +1,207 @@\n+// SPDX-License-Identifier: GPL-2.0\n+//! Test kcov_df_remote_start/stop from kworker context.\n+//!\n+//! A composite struct holds three RBTrees (simulating RBTree/XArray/maple_tree\n+//! workloads). Three work phases run on system_wq:\n+//!   Phase 1 (populate): fill all three trees\n+//!   Phase 2 (update): insert new values, read existing, overwrite\n+//!   Phase 3 (drain): remove all entries\n+//!\n+//! User space publishes a buffer with KCOV_DF_REMOTE_ENABLE, writes to\n+//! /sys/kernel/debug/kcov_dataflow_test/trigger_kworker_remote, then reads\n+//! the captured records.\n+\n+#![allow(missing_docs)]\n+\n+use kernel::prelude::*;\n+use kernel::sync::{Arc, Completion};\n+use kernel::workqueue::{self, impl_has_work, new_work, Work, WorkItem};\n+use kernel::rbtree::RBTree;\n+use kernel::c_str;\n+\n+module! {\n+    type: RustKworkerRemote,\n+    name: \"rust_kworker_remote\",\n+    authors: [\"kcov-dataflow\"],\n+    description: \"Test kcov_df_remote capturing from kworker (RBTree composite)\",\n+    license: \"GPL\",\n+}\n+\n+// Extern bindings for kcov_dataflow remote API (kernel/kcov_dataflow.c)\n+unsafe extern \"C\" {\n+    fn kcov_df_remote_start(handle: u64);\n+    fn kcov_df_remote_stop();\n+}\n+\n+/// Composite data structure: three trees with different key ranges.\n+/// Simulates a real driver managing multiple lookup tables.\n+struct CompositeStore {\n+    /// Primary index (keys 0..N)\n+    primary: RBTree\u003cu64, u64\u003e,\n+    /// Secondary/auxiliary index (keys 100..N)\n+    aux: RBTree\u003cu64, u64\u003e,\n+    /// Scratch/temp space (keys 200..N)\n+    scratch: RBTree\u003cu64, u64\u003e,\n+}\n+\n+impl CompositeStore {\n+    fn new() -\u003e Self {\n+        Self {\n+            primary: RBTree::new(),\n+            aux: RBTree::new(),\n+            scratch: RBTree::new(),\n+        }\n+    }\n+\n+    /// Phase 1: populate all three trees with initial data.\n+    #[inline(never)]\n+    fn populate(\u0026mut self) -\u003e Result {\n+        for i in 0u64..8 {\n+            self.primary.try_create_and_insert(i, i * 0x1111, GFP_KERNEL)?;\n+        }\n+        for i in 100u64..108 {\n+            self.aux.try_create_and_insert(i, i * 0x2222, GFP_KERNEL)?;\n+        }\n+        for i in 200u64..208 {\n+            self.scratch.try_create_and_insert(i, i * 0x3333, GFP_KERNEL)?;\n+        }\n+        Ok(())\n+    }\n+\n+    /// Phase 2: insert more, read existing, overwrite some.\n+    #[inline(never)]\n+    fn update(\u0026mut self) -\u003e Result {\n+        // Insert new entries into primary\n+        for i in 8u64..12 {\n+            self.primary.try_create_and_insert(i, i * 0x4444, GFP_KERNEL)?;\n+        }\n+        // Read from aux (get passes \u0026K which is a struct arg)\n+        for i in 100u64..108 {\n+            let _ = self.aux.get(\u0026i);\n+        }\n+        // Overwrite scratch entries\n+        for i in 200u64..204 {\n+            self.scratch.remove(\u0026i);\n+            self.scratch.try_create_and_insert(i, i * 0x5555, GFP_KERNEL)?;\n+        }\n+        Ok(())\n+    }\n+\n+    /// Phase 3: drain all trees.\n+    #[inline(never)]\n+    fn drain(\u0026mut self) {\n+        while let Some(c) = self.primary.cursor_front_mut() {\n+            c.remove_current();\n+        }\n+        while let Some(c) = self.aux.cursor_front_mut() {\n+            c.remove_current();\n+        }\n+        while let Some(c) = self.scratch.cursor_front_mut() {\n+            c.remove_current();\n+        }\n+    }\n+}\n+\n+/// Work item that runs three phases in kworker context with remote capture.\n+#[pin_data]\n+struct RemoteWork {\n+    #[pin]\n+    work: Work\u003cRemoteWork\u003e,\n+    #[pin]\n+    done: Completion,\n+}\n+\n+impl_has_work! {\n+    impl HasWork\u003cSelf\u003e for RemoteWork { self.work }\n+}\n+\n+impl RemoteWork {\n+    fn new() -\u003e Result\u003cArc\u003cSelf\u003e\u003e {\n+        Arc::pin_init(pin_init!(RemoteWork {\n+            work \u003c- new_work!(\"RemoteWork::work\"),\n+            done \u003c- Completion::new(),\n+        }), GFP_KERNEL)\n+    }\n+}\n+\n+impl WorkItem for RemoteWork {\n+    type Pointer = Arc\u003cRemoteWork\u003e;\n+\n+    fn run(this: Arc\u003cRemoteWork\u003e) {\n+        // Enable remote kcov_dataflow capture for this kworker task.\n+        // SAFETY: FFI call to exported kernel symbol; no-op if no buffer published.\n+        // Handle 1 matches what trigger-view.py passes via KCOV_DF_REMOTE_ENABLE.\n+        unsafe { kcov_df_remote_start(1) };\n+\n+        let mut store = CompositeStore::new();\n+        let _ = store.populate();\n+        let _ = store.update();\n+        store.drain();\n+\n+        // SAFETY: FFI call to exported kernel symbol; disables capture.\n+        unsafe { kcov_df_remote_stop() };\n+\n+        this.done.complete_all();\n+    }\n+}\n+\n+// --- Debugfs trigger (same raw pattern as eight_struct_args_rust) ---\n+\n+unsafe extern \"C\" fn write_handler(\n+    _file: *mut kernel::bindings::file,\n+    _buf: *const core::ffi::c_char,\n+    count: usize,\n+    _ppos: *mut kernel::bindings::loff_t,\n+) -\u003e kernel::ffi::c_long {\n+    let work = match RemoteWork::new() {\n+        Ok(w) =\u003e w,\n+        Err(_) =\u003e return -(kernel::bindings::ENOMEM as kernel::ffi::c_long),\n+    };\n+    let waiter = work.clone();\n+    let _ = workqueue::system().enqueue(work);\n+    waiter.done.wait_for_completion();\n+    count as kernel::ffi::c_long\n+}\n+\n+#[repr(transparent)]\n+struct SyncFops(kernel::bindings::file_operations);\n+unsafe impl Sync for SyncFops {}\n+\n+static FOPS: SyncFops = SyncFops(kernel::bindings::file_operations {\n+    write: Some(unsafe { core::mem::transmute(write_handler as *const ()) }),\n+    ..unsafe { core::mem::zeroed() }\n+});\n+\n+struct RustKworkerRemote {\n+    dir: *mut kernel::bindings::dentry,\n+}\n+\n+impl kernel::Module for RustKworkerRemote {\n+    fn init(_module: \u0026'static ThisModule) -\u003e Result\u003cSelf\u003e {\n+        let dir = unsafe {\n+            kernel::bindings::debugfs_create_dir(\n+                c_str!(\"kcov_dataflow_test\").as_char_ptr(),\n+                core::ptr::null_mut(),\n+            )\n+        };\n+        unsafe {\n+            kernel::bindings::debugfs_create_file_unsafe(\n+                c_str!(\"trigger_kworker_remote\").as_char_ptr(),\n+                0o222,\n+                dir,\n+                core::ptr::null_mut(),\n+                \u0026FOPS.0,\n+            )\n+        };\n+        Ok(Self { dir })\n+    }\n+}\n+\n+impl Drop for RustKworkerRemote {\n+    fn drop(\u0026mut self) {\n+        unsafe { kernel::bindings::debugfs_remove(self.dir) };\n+    }\n+}\n+\n+unsafe impl Send for RustKworkerRemote {}\n+unsafe impl Sync for RustKworkerRemote {}\ndiff --git a/tools/testing/selftests/kcov_dataflow/settings b/tools/testing/selftests/kcov_dataflow/settings\nnew file mode 100644\nindex 0000000000000..694d70710ff08\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/settings\n@@ -0,0 +1 @@\n+timeout=300\ndiff --git a/tools/testing/selftests/kcov_dataflow/test_modules.py b/tools/testing/selftests/kcov_dataflow/test_modules.py\nnew file mode 100755\nindex 0000000000000..13cb706a06ff6\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/test_modules.py\n@@ -0,0 +1,249 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: GPL-2.0\n+\"\"\"\n+test_modules.py - run the kcov_dataflow test modules, one KTAP test each.\n+\n+Every module is loaded, triggered with recording active and unloaded by\n+trigger-view.py's run_capture(). The records that belong to the module are\n+then compared with the values its trigger function passes and returns, so a\n+test passes only when the instrumented arguments, struct field expansions\n+and return values came back intact through the kcov_dataflow buffer. The\n+module's call tree is echoed as KTAP diagnostics.\n+\n+    ./test_modules.py                 # all modules\n+    ./test_modules.py -t rust_ffi_contract -C 8 --vmlinux vmlinux\n+\n+Modules that were not built (no CONFIG_RUST, no toolchain) are reported as\n+SKIP; a kernel without /sys/kernel/debug/kcov_dataflow skips everything.\n+\"\"\"\n+import argparse\n+import contextlib\n+import importlib.util\n+import io\n+import os\n+import sys\n+\n+HERE = os.path.dirname(os.path.abspath(__file__))\n+sys.path.insert(0, os.path.join(HERE, \"..\", \"kselftest\"))\n+import ksft  # noqa: E402\n+\n+\n+def _load_trigger_view():\n+    spec = importlib.util.spec_from_file_location(\n+        \"trigger_view\", os.path.join(HERE, \"trigger-view.py\"))\n+    mod = importlib.util.module_from_spec(spec)\n+    spec.loader.exec_module(mod)\n+    return mod\n+\n+\n+tv = _load_trigger_view()\n+\n+\n+class Check:\n+    \"\"\"Collects expectation failures for one module.\"\"\"\n+\n+    def __init__(self):\n+        self.failures = []\n+\n+    def eq(self, what, got, want):\n+        if got != want:\n+            self.failures.append(f\"{what}: got {fmt(got)}, want {fmt(want)}\")\n+\n+    def true(self, what, cond):\n+        if not cond:\n+            self.failures.append(what)\n+\n+\n+def fmt(v):\n+    if isinstance(v, list):\n+        return \"[\" + \", \".join(fmt(x) for x in v) + \"]\"\n+    if isinstance(v, int):\n+        return f\"0x{v:x}\"\n+    return str(v)\n+\n+\n+def entries(cap, recs, func):\n+    return [r for r in recs if r[\"type\"] == tv.DF_TYPE_ENTRY and func in cap.funcs(r)]\n+\n+\n+def rets(cap, recs, func):\n+    return [r[\"val\"] for r in recs if r[\"type\"] == tv.DF_TYPE_RET and func in cap.funcs(r)]\n+\n+\n+def flat_sum(n):\n+    \"\"\"sf_n() returns a-\u003ea + b-\u003eb + ... over s1..sn: 0x11 + 0x22 + ...\"\"\"\n+    return sum(0x11 * k for k in range(1, n + 1))\n+\n+\n+def nested_sum(n, _memo={}):\n+    \"\"\"\n+    stf_n()/stpf_n() return field0 (0x11 * n) plus the recursive sums of the\n+    embedded st1..st(n-1); the same values are used for the value-nested and\n+    the pointer-linked towers.\n+    \"\"\"\n+    if n not in _memo:\n+        _memo[n] = 0x11 * n + sum(nested_sum(k) for k in range(1, n))\n+    return _memo[n]\n+\n+\n+def check_struct_family(cap, recs, c, p, flat_ns, stpf8_runs):\n+    \"\"\"\n+    Shared expectations for eight_struct_args_c (p=\"\") and\n+    eight_struct_args_rust (p=\"r\"): @flat_ns are the sf_N called by the\n+    trigger, @stpf8_runs how often the pointer-linked tower is walked.\n+    \"\"\"\n+    for n in flat_ns:\n+        ents = entries(cap, recs, f\"{p}sf_{n}\")\n+        c.true(f\"{p}sf_{n}: ENTRY records\", bool(ents))\n+        for k in range(n):\n+            # arg k is a struct s(k+1) * whose fields are 0x11, 0x22, ...\n+            got = [r[\"vals\"] for r in ents if r[\"arg_idx\"] == k]\n+            c.true(f\"{p}sf_{n} arg[{k}]: ENTRY record\", bool(got))\n+            for vals in got:\n+                c.eq(f\"{p}sf_{n} arg[{k}] expanded fields\", vals,\n+                     [0x11 * (j + 1) for j in range(k + 1)])\n+        # rustc may alias identical bodies (rsf_1 == rstf_1), so the RET\n+        # list can carry the alias's calls too: check every value.\n+        got = rets(cap, recs, f\"{p}sf_{n}\")\n+        c.true(f\"{p}sf_{n} RET values all {fmt(flat_sum(n))}: {fmt(got)}\",\n+               bool(got) and all(v == flat_sum(n) for v in got))\n+\n+    for fam, calls in ((f\"{p}stf\", 1), (f\"{p}stpf\", stpf8_runs)):\n+        for n in range(1, 9):\n+            got = rets(cap, recs, f\"{fam}_{n}\")\n+            c.true(f\"{fam}_{n}: RET records\", bool(got))\n+            c.true(f\"{fam}_{n} RET values all {fmt(nested_sum(n))}: {fmt(got)}\",\n+                   all(v == nested_sum(n) for v in got))\n+        c.eq(f\"{fam}_8 RET count\", len(rets(cap, recs, f\"{fam}_8\")), calls)\n+\n+    for f in (f\"{p}sf_fwd\", f\"{p}sf_fwd_inner\"):\n+        c.eq(f\"{f} RET\", rets(cap, recs, f), [flat_sum(4)])\n+\n+    c.true(f\"{p}sf_ret_struct: ENTRY records\",\n+           bool(entries(cap, recs, f\"{p}sf_ret_struct\")))\n+    c.true(f\"{p}sf_ret_struct: RET record\",\n+           bool(rets(cap, recs, f\"{p}sf_ret_struct\")))\n+\n+\n+def check_eight_struct_args_c(cap, recs, c):\n+    check_struct_family(cap, recs, c, \"\", range(1, 9), stpf8_runs=2)\n+\n+\n+def check_eight_struct_args_rust(cap, recs, c):\n+    check_struct_family(cap, recs, c, \"r\", (1, 2, 4, 8), stpf8_runs=1)\n+\n+\n+def check_rust_ffi_contract(cap, recs, c):\n+    \"\"\"\n+    ffi_alloc_buf(\u0026alloc = {NULL, 0, 0, 0}, 256, 16, is_async=1) records\n+    data_size + offsets_size and returns 0 without filling alloc-\u003ebuffer;\n+    ffi_check_result() then sees {NULL, 0x110, 0, 0}. The records must show\n+    the violated contract at both boundaries.\n+    \"\"\"\n+    ents = entries(cap, recs, \"ffi_alloc_buf\")\n+    by_arg = {r[\"arg_idx\"]: r for r in ents}\n+    c.eq(\"ffi_alloc_buf ENTRY arg indexes\", sorted(by_arg), [0, 1, 2, 3])\n+    if 0 in by_arg:\n+        c.eq(\"ffi_alloc_buf arg[0] struct ffi_alloc fields\",\n+             by_arg[0][\"vals\"], [0, 0, 0, 0])\n+    if 1 in by_arg:\n+        c.eq(\"ffi_alloc_buf arg[1] data_size\", by_arg[1][\"val\"], 256)\n+    if 2 in by_arg:\n+        c.eq(\"ffi_alloc_buf arg[2] offsets_size\", by_arg[2][\"val\"], 16)\n+    if 3 in by_arg:\n+        c.eq(\"ffi_alloc_buf arg[3] is_async\", by_arg[3][\"val\"], 1)\n+    c.eq(\"ffi_alloc_buf RET (claims success)\", rets(cap, recs, \"ffi_alloc_buf\"), [0])\n+\n+    ents = entries(cap, recs, \"ffi_check_result\")\n+    c.true(\"ffi_check_result: ENTRY record\", bool(ents))\n+    for r in ents:\n+        c.eq(\"ffi_check_result arg[0] {buffer NULL: contract violated, \"\n+             \"data_size, free_async, flags}\", r[\"vals\"], [0, 0x110, 0, 0])\n+    got = rets(cap, recs, \"ffi_check_result\")\n+    c.true(f\"ffi_check_result RET -EFAULT: {fmt(got)}\",\n+           len(got) == 1 and got[0] \u0026 0xffffffff == 0xfffffff2)\n+\n+\n+def check_rust_kworker_remote(cap, recs, c):\n+    \"\"\"\n+    The trigger only queues a work item and waits; the records come from the\n+    kworker that called kcov_df_remote_start(REMOTE_HANDLE). All three phases\n+    of CompositeStore must show up (v0-mangled names keep the method names).\n+    \"\"\"\n+    c.true(\"records captured from the kworker\", bool(recs))\n+    names = set().union(*(cap.funcs(r) for r in recs)) if recs else set()\n+    for phase in (\"populate\", \"update\", \"drain\"):\n+        c.true(f\"CompositeStore::{phase} recorded\",\n+               any(\"CompositeStore\" in n and phase in n for n in names))\n+\n+\n+TESTS = (\n+    (\"rust_ffi_contract\", False, check_rust_ffi_contract),\n+    (\"eight_struct_args_c\", False, check_eight_struct_args_c),\n+    (\"eight_struct_args_rust\", False, check_eight_struct_args_rust),\n+    (\"rust_kworker_remote\", True, check_rust_kworker_remote),\n+)\n+\n+\n+def diag_tree(cap, recs, vmlinux):\n+    out = io.StringIO()\n+    with contextlib.redirect_stdout(out):\n+        tv.print_tree(recs, cap.syms, vmlinux, {}, cap.ko_path,\n+                      cap.mod_text_start)\n+    for line in out.getvalue().splitlines():\n+        ksft.print_msg(line)\n+\n+\n+def main():\n+    parser = argparse.ArgumentParser(description=__doc__.split(\"\\n\\n\")[0])\n+    parser.add_argument(\"-t\", \"--test\", action=\"append\",\n+                        help=\"run only this module (repeatable)\")\n+    parser.add_argument(\"-C\", \"--context\", type=int, default=0,\n+                        help=\"echo N records before/after each module record\")\n+    parser.add_argument(\"--vmlinux\", help=\"vmlinux for addr2line and KASLR\")\n+    args = parser.parse_args()\n+\n+    tests = [t for t in TESTS if not args.test or t[0] in args.test]\n+    ksft.print_header()\n+    ksft.set_plan(len(tests))\n+\n+    skip_all = None\n+    if not os.path.exists(tv.KCOV_DF_PATH):\n+        skip_all = f\"{tv.KCOV_DF_PATH} not available (CONFIG_KCOV_DATAFLOW_ARGS/RET)\"\n+    elif os.geteuid() != 0:\n+        skip_all = \"must run as root\"\n+\n+    vmlinux = tv.find_vmlinux(args.vmlinux)\n+    for name, remote, check in tests:\n+        if skip_all:\n+            ksft.test_result_skip(f\"{name}: {skip_all}\")\n+            continue\n+        ko = tv.find_module(name)\n+        if not ko:\n+            ksft.test_result_skip(f\"{name}: {name}.ko not built\")\n+            continue\n+        try:\n+            cap = tv.run_capture(ko, remote=remote, vmlinux=vmlinux,\n+                                 log=ksft.print_msg)\n+        except OSError as e:\n+            ksft.test_result_fail(f\"{name}: {e}\")\n+            continue\n+\n+        recs = cap.module_records()\n+        ksft.print_msg(f\"{name}: {cap.total_words} words, {len(cap.records)} \"\n+                       f\"records, {len(recs)} from {name} \"\n+                       f\"(kaslr_offset=0x{cap.kaslr_offset:x})\")\n+        diag_tree(cap, cap.context_records(args.context) if args.context\n+                  else recs, vmlinux)\n+\n+        c = Check()\n+        check(cap, recs, c)\n+        for f in c.failures:\n+            ksft.print_msg(f\"FAIL {name}: {f}\")\n+        ksft.test_result(not c.failures, name)\n+\n+    ksft.finished()\n+\n+\n+if __name__ == \"__main__\":\n+    main()\ndiff --git a/tools/testing/selftests/kcov_dataflow/trigger-view.py b/tools/testing/selftests/kcov_dataflow/trigger-view.py\nnew file mode 100755\nindex 0000000000000..b17e49da402d7\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/trigger-view.py\n@@ -0,0 +1,755 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: GPL-2.0\n+\"\"\"\n+trigger-view.py - Load a test module, trigger it with kcov_dataflow\n+recording active, then pretty-print the captured records.\n+\n+Usage:\n+    python3 trigger-view.py eight_struct_args_c\n+    python3 trigger-view.py rust_ffi_contract --raw -C 8\n+    python3 trigger-view.py rust_kworker_remote --remote\n+    python3 trigger-view.py \u003cmodule\u003e --vmlinux vmlinux --kaslr-offset 0x...\n+\n+run_capture() does the work and is also what test_modules.py drives:\n+  1. Opens /sys/kernel/debug/kcov_dataflow, inits and mmaps the buffer\n+  2. Loads the module via finit_module() (its init noise is not recorded)\n+  3. Enables recording: KCOV_DF_ENABLE for this task, or with --remote\n+     KCOV_DF_REMOTE_ENABLE with handle REMOTE_HANDLE, which the module's\n+     kworker opens with kcov_df_remote_start(REMOTE_HANDLE)\n+  4. Writes the trigger file(s) the module created under TRIGGER_DIR\n+  5. Disables recording and unloads the module\n+  6. Parses the records (layout: include/uapi/linux/kcov_dataflow.h)\n+\n+The CLI then prints them as a call tree, or flat with --raw, with kallsyms\n+symbol resolution and addr2line source lines (vmlinux / module .ko).\n+\n+Recorded PCs have the KASLR offset removed (same as mainline kcov), so\n+the runtime offset is derived from /proc/kallsyms and System.map / vmlinux\n+(or a per-architecture default) and added back for symbolization; use\n+--kaslr-offset to override. Records must contain at least one value word\n+and one of the three record types, otherwise the parser resyncs word by\n+word (e.g. after a userspace reset of area[0] mid-run).\n+\"\"\"\n+import os\n+import sys\n+import struct\n+import ctypes\n+import ctypes.util\n+import argparse\n+import fcntl\n+import platform\n+import subprocess\n+import shutil\n+\n+# Constants -- must match include/uapi/linux/kcov_dataflow.h\n+DF_TYPE_CMP = 0xC\n+DF_TYPE_ENTRY = 0xE\n+DF_TYPE_RET = 0xF\n+MAGIC_BAD = 0xBADADD85\n+BUF_SIZE = 1048576  # 1M words = 8MB\n+\n+# Record header word: bits 0-23 seq | 28-31 type | 32-47 nvals |\n+# 48-55 arg/ret size | 56-63 arg index. Word 1 is the pc (KASLR offset\n+# removed, like mainline kcov), word 2 the traced pointer (ENTRY/RET) or the\n+# comparison type (CMP), then nvals value words.\n+def hdr_seq(h):\n+    return h \u0026 0x00FFFFFF\n+\n+def hdr_type(h):\n+    return (h \u003e\u003e 28) \u0026 0xF\n+\n+def hdr_nvals(h):\n+    return (h \u003e\u003e 32) \u0026 0xFFFF\n+\n+def hdr_size(h):\n+    return (h \u003e\u003e 48) \u0026 0xFF\n+\n+def hdr_arg_idx(h):\n+    return (h \u003e\u003e 56) \u0026 0xFF\n+\n+RECORD_HDR_WORDS = 3\n+\n+# Runtime KASLR offset (see kaslr_offset()); added back to every recorded pc\n+# so /proc/kallsyms lookups work, subtracted again for addr2line on vmlinux.\n+KASLR_OFFSET = 0\n+\n+# Ioctl numbers\n+def _IOR(t, nr, size):\n+    return (2 \u003c\u003c 30) | (ord(t) \u003c\u003c 8) | nr | (size \u003c\u003c 16)\n+\n+def _IOW(t, nr, size):\n+    return (1 \u003c\u003c 30) | (ord(t) \u003c\u003c 8) | nr | (size \u003c\u003c 16)\n+\n+def _IO(t, nr):\n+    return (ord(t) \u003c\u003c 8) | nr\n+\n+KCOV_DF_INIT_TRACK = _IOR('d', 1, 8)\n+KCOV_DF_ENABLE = _IO('d', 100)\n+KCOV_DF_DISABLE = _IO('d', 101)\n+KCOV_DF_REMOTE_ENABLE = _IOW('d', 102, 8)  # arg: pointer to a __u64 handle\n+KCOV_DF_REMOTE_DISABLE = _IO('d', 103)\n+\n+KCOV_DF_PATH = \"/sys/kernel/debug/kcov_dataflow\"\n+\n+# Every test module creates its trigger file(s) in this debugfs directory;\n+# writing to them runs the instrumented test functions.\n+TRIGGER_DIR = \"/sys/kernel/debug/kcov_dataflow_test\"\n+\n+# Remote handle registered with KCOV_DF_REMOTE_ENABLE; must match the\n+# kcov_df_remote_start(1) call in the rust_kworker_remote test module\n+# (KCOV_SUBSYSTEM_COMMON, instance 1).\n+REMOTE_HANDLE = 1\n+\n+# syscall numbers\n+_machine = platform.machine()\n+if _machine == \"aarch64\":\n+    SYS_FINIT_MODULE = 273\n+    SYS_DELETE_MODULE = 106\n+else:  # x86_64\n+    SYS_FINIT_MODULE = 313\n+    SYS_DELETE_MODULE = 176\n+\n+SELFTEST_DIR = os.path.dirname(os.path.abspath(__file__))\n+\n+\n+def load_kallsyms():\n+    \"\"\"Load kernel symbols for PC resolution.\"\"\"\n+    syms = []\n+    try:\n+        with open(\"/proc/kallsyms\") as f:\n+            for line in f:\n+                parts = line.split()\n+                if len(parts) \u003e= 3:\n+                    addr = int(parts[0], 16)\n+                    name = parts[2]\n+                    mod = parts[3].strip(\"[]\") if len(parts) \u003e 3 else \"\"\n+                    syms.append((addr, name, mod))\n+    except (PermissionError, FileNotFoundError):\n+        pass\n+    syms.sort()\n+    return syms\n+\n+\n+def runtime_text(syms):\n+    \"\"\"Runtime address of _text from kallsyms, 0 if hidden.\"\"\"\n+    return next((a for a, n, m in syms if n == \"_text\" and not m), 0)\n+\n+\n+# Link-time address of _text per architecture, used only when neither\n+# System.map nor vmlinux is available: x86_64 __START_KERNEL\n+# (__START_KERNEL_map + CONFIG_PHYSICAL_START), arm64 KIMAGE_VADDR.\n+LINKTIME_TEXT_DEFAULT = {\n+    \"x86_64\": 0xffffffff81000000,\n+    \"aarch64\": 0xffff800080000000,\n+}\n+\n+\n+def linktime_text(vmlinux=None):\n+    \"\"\"Return (link-time address of _text, source description) or (0, \"\").\"\"\"\n+    rel = os.uname().release\n+    candidates = []\n+    if vmlinux:\n+        candidates.append(os.path.join(os.path.dirname(vmlinux) or \".\", \"System.map\"))\n+    candidates += [\"System.map\", f\"/boot/System.map-{rel}\",\n+                   f\"/usr/lib/debug/boot/System.map-{rel}\"]\n+    for sm in candidates:\n+        try:\n+            with open(sm) as f:\n+                for line in f:\n+                    parts = line.split()\n+                    if len(parts) == 3 and parts[2] == \"_text\":\n+                        return int(parts[0], 16), sm\n+        except (OSError, ValueError):\n+            continue\n+    if vmlinux and shutil.which(\"nm\"):\n+        try:\n+            r = subprocess.run([\"nm\", \"--defined-only\", vmlinux],\n+                               capture_output=True, text=True, timeout=300)\n+            for line in r.stdout.splitlines():\n+                parts = line.split()\n+                if len(parts) == 3 and parts[2] == \"_text\":\n+                    return int(parts[0], 16), f\"nm {vmlinux}\"\n+        except (OSError, subprocess.TimeoutExpired):\n+            pass\n+    link = LINKTIME_TEXT_DEFAULT.get(platform.machine(), 0)\n+    return link, f\"{platform.machine()} default\" if link else \"\"\n+\n+\n+def kaslr_offset(syms, vmlinux=None):\n+    \"\"\"\n+    Runtime KASLR offset: recorded PCs have it removed (kcov's\n+    canonicalize_ip()), /proc/kallsyms has it applied. Computed as the\n+    runtime _text (kallsyms) minus the link-time _text (System.map, nm\n+    vmlinux, or the architecture default). KASLR offsets are 2 MiB aligned\n+    on x86_64 and arm64, which is used as a sanity check on the result.\n+    \"\"\"\n+    runtime = runtime_text(syms)\n+    if not runtime:\n+        print(\"# warning: _text not in /proc/kallsyms (kptr_restrict?); \"\n+              \"PCs will not symbolize\", file=sys.stderr)\n+        return 0\n+    link, source = linktime_text(vmlinux)\n+    if not link:\n+        print(f\"# warning: no System.map/vmlinux and no default _text for \"\n+              f\"{platform.machine()}; pass --kaslr-offset\", file=sys.stderr)\n+        return 0\n+    off = runtime - link\n+    if off % (2 \u003c\u003c 20):\n+        print(f\"# warning: kaslr offset 0x{off:x} from {source} is not 2 MiB \"\n+              f\"aligned; check CONFIG_PHYSICAL_START/KIMAGE_VADDR or pass \"\n+              f\"--kaslr-offset\", file=sys.stderr)\n+    return off\n+\n+\n+# Rust symbol demangling via llvm-cxxfilt or rustfilt\n+_demangler = None\n+\n+def _init_demangler():\n+    global _demangler\n+    for tool in [\"llvm-cxxfilt\", \"rustfilt\", \"c++filt\"]:\n+        path = shutil.which(tool)\n+        if path:\n+            _demangler = path\n+            return\n+    _demangler = \"\"\n+\n+_demangled = {}\n+\n+def demangle(name):\n+    \"\"\"Demangle a Rust/C++ symbol name (memoized: one process per name).\"\"\"\n+    global _demangler\n+    if _demangler is None:\n+        _init_demangler()\n+    if not _demangler or not name.startswith(\"_R\"):\n+        return name\n+    if name not in _demangled:\n+        try:\n+            r = subprocess.run([_demangler, name], capture_output=True,\n+                               text=True, timeout=2)\n+            _demangled[name] = r.stdout.strip() if r.returncode == 0 else name\n+        except (OSError, subprocess.TimeoutExpired):\n+            _demangled[name] = name\n+    return _demangled[name]\n+\n+\n+def find_vmlinux(vmlinux=None):\n+    \"\"\"Locate vmlinux for addr2line: explicit path, else the usual places.\"\"\"\n+    if vmlinux:\n+        return vmlinux\n+    for p in [\"vmlinux\", \"/boot/vmlinux\", \"/usr/lib/debug/boot/vmlinux\"]:\n+        if os.path.exists(p):\n+            return p\n+    return None\n+\n+\n+def _a2l_target(pc, vmlinux, ko_path, mod_text_base):\n+    \"\"\"(binary, address in it) to symbolize pc with, or None.\"\"\"\n+    if ko_path and mod_text_base and pc \u003e= mod_text_base:\n+        return ko_path, pc - mod_text_base\n+    if vmlinux:\n+        return vmlinux, pc - KASLR_OFFSET  # vmlinux holds link-time addresses\n+    return None\n+\n+\n+def resolve_lines(pcs, vmlinux, cache, ko_path=None, mod_text_base=0):\n+    \"\"\"\n+    Resolve every pc in @pcs to file:line into @cache, one addr2line run\n+    per binary: a DWARF5 vmlinux takes hundreds of ms to open, so one\n+    process per record does not scale to thousands of records.\n+    \"\"\"\n+    todo = {}\n+    for pc in pcs:\n+        if pc in cache:\n+            continue\n+        cache[pc] = \"\"\n+        tgt = _a2l_target(pc, vmlinux, ko_path, mod_text_base)\n+        if tgt:\n+            todo.setdefault(tgt[0], []).append((pc, tgt[1]))\n+    for binary, pairs in todo.items():\n+        try:\n+            r = subprocess.run(\n+                [\"addr2line\", \"-e\", binary] + [f\"0x{a:x}\" for _, a in pairs],\n+                capture_output=True, text=True, timeout=300)\n+        except (subprocess.TimeoutExpired, FileNotFoundError):\n+            continue\n+        for (pc, _), loc in zip(pairs, r.stdout.splitlines()):\n+            loc = loc.strip()\n+            if loc and loc != \"??:0\" and loc != \"??:?\":\n+                # Shorten path: keep only filename:line\n+                cache[pc] = loc.rsplit(\"/\", 1)[-1]\n+\n+\n+def resolve_line(pc, vmlinux, cache, ko_path=None, mod_text_base=0):\n+    \"\"\"Resolve one PC to source file:line using addr2line (cached).\"\"\"\n+    if pc not in cache:\n+        resolve_lines([pc], vmlinux, cache, ko_path, mod_text_base)\n+    return cache[pc]\n+\n+\n+def get_kernel_meta():\n+    \"\"\"Collect kernel build metadata.\"\"\"\n+    meta = {\"release\": os.uname().release}\n+    try:\n+        with open(\"/proc/version\") as f:\n+            v = f.read().strip()\n+        meta[\"version\"] = v\n+        # Extract compiler version\n+        if \"gcc\" in v.lower():\n+            meta[\"compiler\"] = v.split(\"(\")[1].split(\")\")[0] if \"(\" in v else \"\"\n+        elif \"clang\" in v.lower():\n+            idx = v.lower().find(\"clang\")\n+            meta[\"compiler\"] = v[idx:idx+30].split(\")\")[0]\n+    except OSError:\n+        pass\n+    return meta\n+\n+\n+def print_kernel_meta(meta, ko_path=None):\n+    \"\"\"Print kernel metadata header/footer.\"\"\"\n+    print(f\"# {'=' * 60}\")\n+    print(f\"# Kernel: {meta.get('release', 'unknown')}\")\n+    print(f\"# Build:  {meta.get('version', 'unknown')[:80]}\")\n+    if meta.get('compiler'):\n+        print(f\"# Compiler: {meta['compiler']}\")\n+    # Read rustc version from .ko .comment section\n+    if ko_path:\n+        try:\n+            r = subprocess.run(\n+                [\"readelf\", \"-p\", \".comment\", ko_path],\n+                capture_output=True, text=True, timeout=5)\n+            for line in r.stdout.splitlines():\n+                if \"rustc\" in line:\n+                    ver = line.split(\"]\", 1)[-1].strip()\n+                    print(f\"# Rustc: {ver}\")\n+                    break\n+        except (OSError, subprocess.TimeoutExpired):\n+            pass\n+    print(f\"# {'=' * 60}\")\n+\n+\n+def lookup(pc, syms):\n+    \"\"\"Nearest kallsyms entry \u003c= pc as (name, offset, module) or None.\"\"\"\n+    if not syms:\n+        return None\n+    lo, hi = 0, len(syms) - 1\n+    while lo \u003c hi:\n+        mid = (lo + hi + 1) // 2\n+        if syms[mid][0] \u003c= pc:\n+            lo = mid\n+        else:\n+            hi = mid - 1\n+    addr, name, mod = syms[lo]\n+    if addr \u003e pc:\n+        return None\n+    return name, pc - addr, mod\n+\n+\n+def symbolize(pc, syms):\n+    \"\"\"Find nearest symbol \u003c= pc. Returns (display_name, module_tag).\"\"\"\n+    hit = lookup(pc, syms)\n+    if not hit:\n+        return f\"0x{pc:x}\", \"\"\n+    name, offset, mod = hit\n+    dname = demangle(name)\n+    display = f\"{dname}+0x{offset:x}\" if offset else dname\n+    return display, f\" [{mod}]\" if mod else \"\"\n+\n+\n+def format_val(v):\n+    \"\"\"Format a captured value.\"\"\"\n+    if v == MAGIC_BAD:\n+        return \"FAULT\"\n+    if v == 0:\n+        return \"0x0\"\n+    return f\"0x{v:x}\"\n+\n+\n+def find_module(name):\n+    \"\"\"\n+    Find the .ko for test @name: \u003cname\u003e/\u003cname\u003e.ko in the source tree, or\n+    \u003cname\u003e.ko next to this script in an installed (make install) tree.\n+    \"\"\"\n+    for ko_path in (os.path.join(SELFTEST_DIR, name, f\"{name}.ko\"),\n+                    os.path.join(SELFTEST_DIR, f\"{name}.ko\")):\n+        if os.path.exists(ko_path):\n+            return ko_path\n+    return None\n+\n+\n+def finit_module(ko_path):\n+    \"\"\"Load a kernel module via finit_module syscall.\"\"\"\n+    libc = ctypes.CDLL(ctypes.util.find_library(\"c\"), use_errno=True)\n+    fd = os.open(ko_path, os.O_RDONLY)\n+    ret = libc.syscall(SYS_FINIT_MODULE, fd, b\"\", 0)\n+    os.close(fd)\n+    if ret != 0:\n+        errno = ctypes.get_errno()\n+        raise OSError(errno, f\"finit_module({ko_path}): {os.strerror(errno)}\")\n+\n+\n+def delete_module(name):\n+    \"\"\"Unload a kernel module.\"\"\"\n+    libc = ctypes.CDLL(ctypes.util.find_library(\"c\"), use_errno=True)\n+    ret = libc.syscall(SYS_DELETE_MODULE, name.encode(), 0)\n+    if ret != 0:\n+        errno = ctypes.get_errno()\n+        raise OSError(errno, f\"delete_module({name}): {os.strerror(errno)}\")\n+\n+\n+def trigger_module():\n+    \"\"\"\n+    Write to every trigger file the loaded module created under TRIGGER_DIR.\n+    Opened without O_CREAT: debugfs directories have no -\u003ecreate, so a\n+    \"w\"-mode open of a missing name fails with EOPNOTSUPP, not ENOENT.\n+    \"\"\"\n+    try:\n+        names = sorted(os.listdir(TRIGGER_DIR))\n+    except OSError:\n+        names = []\n+    hits = []\n+    for n in names:\n+        path = os.path.join(TRIGGER_DIR, n)\n+        try:\n+            fd = os.open(path, os.O_WRONLY)\n+        except OSError:\n+            continue\n+        try:\n+            os.write(fd, b\"1\")\n+        finally:\n+            os.close(fd)\n+        hits.append(path)\n+    if not hits:\n+        raise FileNotFoundError(f\"no trigger file under {TRIGGER_DIR}\")\n+    return hits\n+\n+\n+def parse_records(buf, total_words):\n+    \"\"\"Parse the ring buffer into a list of records.\"\"\"\n+    records = []\n+    pos = 1\n+    end = min(1 + total_words, BUF_SIZE)\n+    while pos + RECORD_HDR_WORDS \u003c= end:\n+        hdr = buf[pos]\n+        rtype = hdr_type(hdr)\n+        num_vals = hdr_nvals(hdr)\n+\n+        # Every record the kernel writes has nvals \u003e= 1 and a known type;\n+        # anything else is garbage (e.g. a userspace reset mid-run): resync.\n+        if rtype not in (DF_TYPE_ENTRY, DF_TYPE_RET, DF_TYPE_CMP) \\\n+                or num_vals == 0 or pos + RECORD_HDR_WORDS + num_vals \u003e end:\n+            pos += 1\n+            continue\n+\n+        pc = int(buf[pos + 1]) + KASLR_OFFSET\n+        ptr = int(buf[pos + 2])  # ENTRY/RET: traced pointer; CMP: cmp type\n+        if rtype == DF_TYPE_CMP:\n+            pos += RECORD_HDR_WORDS + num_vals\n+            continue\n+\n+        # Valid records always have a non-zero PC (kernel text address)\n+        if pc == 0:\n+            pos += 1\n+            continue\n+\n+        vals = [int(buf[pos + RECORD_HDR_WORDS + vi]) for vi in range(num_vals)]\n+        records.append({\n+            \"type\": rtype,\n+            \"seq\": hdr_seq(hdr),\n+            \"pc\": pc,\n+            \"ptr\": ptr,\n+            \"arg_idx\": hdr_arg_idx(hdr),\n+            \"size\": hdr_size(hdr),\n+            \"val\": vals[0],\n+            \"vals\": vals,\n+        })\n+        pos += RECORD_HDR_WORDS + num_vals\n+    return records\n+\n+\n+class Capture:\n+    \"\"\"Everything run_capture() collected for one module run.\"\"\"\n+\n+    def __init__(self, ko_path, mod_name, records, syms, total_words,\n+                 mod_text_start, kaslr_off):\n+        self.ko_path = ko_path\n+        self.mod_name = mod_name\n+        self.records = records\n+        self.syms = syms\n+        self.total_words = total_words\n+        self.mod_text_start = mod_text_start\n+        self.kaslr_offset = kaslr_off\n+        self.runtime_text = runtime_text(syms)\n+        self._mod_syms = any(m == mod_name for _, _, m in syms)\n+        # Aliases: rustc's merge-functions makes identical bodies (e.g. the\n+        # one-field rsf_1 and rstf_1) share one address, so a PC can carry\n+        # several names.\n+        self._names = {}\n+        for addr, name, mod in syms:\n+            self._names.setdefault((addr, mod), set()).add(name)\n+\n+    def is_module_pc(self, pc):\n+        \"\"\"True if pc lies in the test module (kallsyms, else .text start).\"\"\"\n+        if self._mod_syms:\n+            hit = lookup(pc, self.syms)\n+            return bool(hit) and hit[2] == self.mod_name\n+        # Fallback: if no module symbols (kptr_restrict), use .text start\n+        return bool(self.mod_text_start) and pc \u003e= self.mod_text_start\n+\n+    def funcs(self, rec):\n+        \"\"\"All raw kallsyms names of the function a record belongs to.\"\"\"\n+        hit = lookup(rec[\"pc\"], self.syms)\n+        if not hit:\n+            return set()\n+        name, offset, mod = hit\n+        return self._names.get((rec[\"pc\"] - offset, mod), {name})\n+\n+    def module_records(self):\n+        return [r for r in self.records if self.is_module_pc(r[\"pc\"])]\n+\n+    def context_records(self, n):\n+        \"\"\"Module records plus n records before/after each of them.\"\"\"\n+        keep = set()\n+        for i, r in enumerate(self.records):\n+            if self.is_module_pc(r[\"pc\"]):\n+                keep.update(range(max(0, i - n),\n+                                  min(len(self.records), i + n + 1)))\n+        return [self.records[i] for i in sorted(keep)]\n+\n+\n+def run_capture(ko_path, remote=False, vmlinux=None, kaslr_override=None,\n+                log=None):\n+    \"\"\"\n+    Load @ko_path, record while its trigger file(s) are written, unload it\n+    and return a Capture. @remote publishes the buffer for REMOTE_HANDLE\n+    instead of enabling recording for this task. Raises OSError.\n+    \"\"\"\n+    global KASLR_OFFSET\n+    log = log or (lambda msg: print(f\"# {msg}\"))\n+\n+    # Ensure kallsyms shows real addresses\n+    try:\n+        with open(\"/proc/sys/kernel/kptr_restrict\", \"w\") as f:\n+            f.write(\"0\")\n+    except OSError:\n+        pass\n+\n+    df_fd = os.open(KCOV_DF_PATH, os.O_RDWR)\n+    try:\n+        # Init + mmap\n+        fcntl.ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)\n+        libc = ctypes.CDLL(ctypes.util.find_library(\"c\"), use_errno=True)\n+        libc.mmap.restype = ctypes.c_void_p\n+        libc.mmap.argtypes = [\n+            ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,\n+            ctypes.c_int, ctypes.c_int, ctypes.c_long\n+        ]\n+        buf_ptr = libc.mmap(None, BUF_SIZE * 8, 0x3, 0x01, df_fd, 0)\n+        if buf_ptr == ctypes.c_void_p(-1).value:\n+            errno = ctypes.get_errno()\n+            raise OSError(errno, f\"mmap: {os.strerror(errno)}\")\n+        buf = (ctypes.c_uint64 * BUF_SIZE).from_address(buf_ptr)\n+\n+        # Load module first (its init generates noise with INSTRUMENT_ALL)\n+        mod_name = os.path.basename(ko_path).replace(\".ko\", \"\")\n+        finit_module(ko_path)\n+        log(f\"Loaded {mod_name}\")\n+        try:\n+            # Module .text address, the PC filter fallback without kallsyms\n+            mod_text_start = 0\n+            try:\n+                with open(f\"/sys/module/{mod_name}/sections/.text\") as f:\n+                    mod_text_start = int(f.read().strip(), 16)\n+            except (OSError, ValueError):\n+                pass\n+\n+            # Enable recording AFTER load, BEFORE trigger (no loader noise).\n+            # Remote: the handle is passed by pointer (a __u64 in a buffer),\n+            # so the full 64-bit value survives 32-bit/compat callers.\n+            if remote:\n+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_ENABLE,\n+                            struct.pack(\"Q\", REMOTE_HANDLE))\n+            else:\n+                fcntl.ioctl(df_fd, KCOV_DF_ENABLE, 0)\n+            buf[0] = 0\n+            try:\n+                for path in trigger_module():\n+                    log(f\"Triggered {path}\")\n+            finally:\n+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_DISABLE if remote\n+                            else KCOV_DF_DISABLE, 0)\n+\n+            # Read kallsyms while the module is still loaded\n+            syms = load_kallsyms()\n+        finally:\n+            try:\n+                delete_module(mod_name)\n+            except OSError as e:\n+                log(f\"warning: {e}\")\n+\n+        if kaslr_override is not None:\n+            KASLR_OFFSET = kaslr_override\n+        else:\n+            KASLR_OFFSET = kaslr_offset(syms, find_vmlinux(vmlinux))\n+\n+        total = int(buf[0])\n+        records = parse_records(buf, total)\n+        return Capture(ko_path, mod_name, records, syms, total,\n+                       mod_text_start, KASLR_OFFSET)\n+    finally:\n+        os.close(df_fd)\n+\n+\n+def print_raw(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):\n+    \"\"\"Print records in raw format with source line on left.\"\"\"\n+    if cache is None:\n+        cache = {}\n+    # Pre-resolve all locations (one addr2line run) to find max width\n+    resolve_lines([r[\"pc\"] for r in records], vmlinux, cache, ko_path,\n+                  mod_text_base)\n+    locs = [cache[r[\"pc\"]] for r in records]\n+    max_w = max((len(l) for l in locs if l), default=0)\n+    max_w = max(max_w, 10)  # minimum width\n+\n+    for i, r in enumerate(records):\n+        name, mod = symbolize(r[\"pc\"], syms)\n+        sym = f\"{name}{mod}\"\n+        t = \"ENTRY\" if r[\"type\"] == DF_TYPE_ENTRY else \"RET  \"\n+        arg_idx = r[\"arg_idx\"]\n+        size = r[\"size\"]\n+        left = f\"{locs[i]:\u003e{max_w}s}\" if locs[i] else f\"{'':\u003e{max_w}s}\"\n+        vals = format_val(r[\"val\"]) if len(r[\"vals\"]) == 1 else \\\n+            \"{\" + \", \".join(format_val(v) for v in r[\"vals\"]) + \"}\"\n+        print(f\"{left}   [{t}] seq={r['seq']:3d} {sym} \"\n+              f\"arg[{arg_idx}]({size}) @0x{r['ptr']:x} = {vals}\")\n+\n+\n+def print_tree(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):\n+    \"\"\"Print records as indented call tree with source line on left.\"\"\"\n+    if cache is None:\n+        cache = {}\n+    # Pre-resolve all PCs (one addr2line run) for alignment\n+    resolve_lines([r[\"pc\"] for r in records], vmlinux, cache, ko_path,\n+                  mod_text_base)\n+    max_w = max((len(v) for v in cache.values() if v), default=10)\n+    max_w = max(max_w, 10)\n+\n+    depth = 0\n+    call_stack = []  # Stack of (name, mod, args_str, pc) for matching returns\n+    i = 0\n+    while i \u003c len(records):\n+        r = records[i]\n+        name, mod = symbolize(r[\"pc\"], syms)\n+\n+        if r[\"type\"] == DF_TYPE_ENTRY:\n+            # Collect all args for this call (same PC, consecutive entries);\n+            # order by index, as the pass emits dead-arg traces last.\n+            args = []\n+            pc = r[\"pc\"]\n+            while i \u003c len(records) and records[i][\"type\"] == DF_TYPE_ENTRY \\\n+                    and records[i][\"pc\"] == pc:\n+                vals = records[i][\"vals\"]\n+                if len(vals) \u003e 1:\n+                    fields = \", \".join(format_val(v) for v in vals)\n+                    args.append((records[i][\"arg_idx\"], \"{\" + fields + \"}\"))\n+                else:\n+                    args.append((records[i][\"arg_idx\"],\n+                                 format_val(records[i][\"val\"])))\n+                i += 1\n+            args_str = \", \".join(a for _, a in sorted(args, key=lambda x: x[0]))\n+            call_stack.append((name, mod, args_str, pc))\n+            depth += 1\n+        else:\n+            # Pop void calls (no return record) until we find matching PC\n+            while call_stack and call_stack[-1][3] != r[\"pc\"]:\n+                depth = max(0, depth - 1)\n+                indent = \"  \" * depth\n+                vname, vmod, vargs, vpc = call_stack.pop()\n+                loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)\n+                left = f\"{loc:\u003e{max_w}s}\" if loc else f\"{'':\u003e{max_w}s}\"\n+                print(f\"{left}   {indent}{vname}({vargs}){vmod}\")\n+            depth = max(0, depth - 1)\n+            indent = \"  \" * depth\n+            ret_size = r[\"size\"]\n+            loc = resolve_line(r[\"pc\"], vmlinux, cache, ko_path, mod_text_base)\n+            left = f\"{loc:\u003e{max_w}s}\" if loc else f\"{'':\u003e{max_w}s}\"\n+            if call_stack:\n+                cname, cmod, cargs, _ = call_stack.pop()\n+                if ret_size == 0:\n+                    print(f\"{left}   {indent}{cname}({cargs}){cmod}\")\n+                else:\n+                    print(f\"{left}   {indent}{format_val(r['val'])} = {cname}({cargs}){cmod}\")\n+            else:\n+                if ret_size == 0:\n+                    print(f\"{left}   {indent}{name}(){mod}\")\n+                else:\n+                    print(f\"{left}   {indent}{format_val(r['val'])} = {name}(){mod}\")\n+            i += 1\n+\n+    # Flush remaining void calls on the stack\n+    while call_stack:\n+        depth = max(0, depth - 1)\n+        indent = \"  \" * depth\n+        vname, vmod, vargs, vpc = call_stack.pop()\n+        loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)\n+        left = f\"{loc:\u003e{max_w}s}\" if loc else f\"{'':\u003e{max_w}s}\"\n+        print(f\"{left}   {indent}{vname}({vargs}){vmod}\")\n+\n+\n+def main():\n+    parser = argparse.ArgumentParser(\n+        description=\"Load a test module with kcov_dataflow and view records\")\n+    parser.add_argument(\"module\", help=\"Test module name (e.g. eight_struct_args_c)\")\n+    parser.add_argument(\"--raw\", action=\"store_true\",\n+                        help=\"Print raw records instead of tree\")\n+    parser.add_argument(\"--ko\", help=\"Explicit path to .ko file\")\n+    parser.add_argument(\"--context\", \"-C\", type=int, default=0,\n+                        help=\"Show N records before/after each module record\")\n+    parser.add_argument(\"--vmlinux\", help=\"Path to vmlinux for addr2line\")\n+    parser.add_argument(\"--remote\", action=\"store_true\",\n+                        help=\"Use KCOV_DF_REMOTE_ENABLE for kworker capture\")\n+    parser.add_argument(\"--kaslr-offset\", type=lambda x: int(x, 0),\n+                        help=\"Override the runtime KASLR offset added to PCs\")\n+    args = parser.parse_args()\n+\n+    ko_path = args.ko or find_module(args.module)\n+    if not ko_path or not os.path.exists(ko_path):\n+        print(f\"Cannot find module for '{args.module}'\", file=sys.stderr)\n+        print(\"Build it first: make -C tools/testing/selftests \"\n+              \"TARGETS=kcov_dataflow LLVM=1 CC=clang\", file=sys.stderr)\n+        sys.exit(1)\n+\n+    try:\n+        cap = run_capture(ko_path, remote=args.remote, vmlinux=args.vmlinux,\n+                          kaslr_override=args.kaslr_offset)\n+    except OSError as e:\n+        print(f\"{args.module}: {e}\", file=sys.stderr)\n+        sys.exit(1)\n+\n+    print(f\"# Captured {cap.total_words} words (kaslr_offset=0x{cap.kaslr_offset:x}, \"\n+          f\"_text=0x{cap.runtime_text:x})\")\n+    print(f\"# {len(cap.records)} records\")\n+\n+    if cap.syms or cap.mod_text_start:\n+        if args.context \u003e 0:\n+            records = cap.context_records(args.context)\n+            print(f\"# showing {len(records)} records with context={args.context} \"\n+                  f\"around {cap.mod_name}\\n\")\n+        else:\n+            records = cap.module_records()\n+            print(f\"# {len(records)} from {cap.mod_name}\\n\")\n+    else:\n+        records = cap.records\n+        print(\"\")\n+\n+    meta = get_kernel_meta()\n+    print_kernel_meta(meta, ko_path=ko_path)\n+\n+    vmlinux = find_vmlinux(args.vmlinux)\n+    show = print_raw if args.raw else print_tree\n+    show(records, cap.syms, vmlinux, {}, ko_path, cap.mod_text_start)\n+\n+    print_kernel_meta(meta, ko_path=ko_path)\n+\n+\n+if __name__ == \"__main__\":\n+    main()\ndiff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile b/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile\nnew file mode 100644\nindex 0000000000000..1cb3d9b41c070\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/Makefile\n@@ -0,0 +1,5 @@\n+# SPDX-License-Identifier: GPL-2.0\n+# Standalone build of the ioctl test: make -C tools/testing/selftests/kcov_dataflow/user_ioctl\n+TEST_GEN_PROGS := user_ioctl\n+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)\n+include ../../lib.mk\ndiff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst b/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst\nnew file mode 100644\nindex 0000000000000..55072de189d31\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/README.rst\n@@ -0,0 +1,11 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+KCOV-Dataflow Selftests: user_ioctl\n+===================================\n+\n+Automated ioctl interface test (kselftest harness, 9 TAP cases): INIT_TRACK\n+argument checking, double init, mmap before init, ENABLE/DISABLE pairing,\n+a second fd failing with -EBUSY, and record validity after a syscall::\n+\n+  make -C tools/testing/selftests TARGETS=kcov_dataflow\n+  tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\ndiff --git a/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c b/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c\nnew file mode 100644\nindex 0000000000000..d7b04c368ced9\n--- /dev/null\n+++ b/tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c\n@@ -0,0 +1,168 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * kcov_dataflow_test.c - Selftest for /sys/kernel/debug/kcov_dataflow\n+ *\n+ * Verifies the ioctl interface: open, INIT_TRACK, mmap, ENABLE, DISABLE.\n+ * With INSTRUMENT_ALL, also verifies that records are produced for\n+ * syscalls executed while recording is active.\n+ */\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cerrno.h\u003e\n+#include \u003clinux/kcov_dataflow.h\u003e\n+\n+#include \"../../kselftest_harness.h\"\n+\n+\n+#define BUF_SIZE 65536\n+\n+#define DF_TYPE_ENTRY\tKCOV_DF_TYPE_ENTRY\n+#define DF_TYPE_RET\tKCOV_DF_TYPE_RET\n+\n+FIXTURE(kcov_dataflow) {\n+\tint fd;\n+\tuint64_t *buf;\n+};\n+\n+FIXTURE_SETUP(kcov_dataflow)\n+{\n+\tself-\u003efd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+\tif (self-\u003efd \u003c 0)\n+\t\tSKIP(return, \"kcov_dataflow not available (need CONFIG_KCOV_DATAFLOW_ARGS)\");\n+\tself-\u003ebuf = MAP_FAILED;\n+}\n+\n+FIXTURE_TEARDOWN(kcov_dataflow)\n+{\n+\tif (self-\u003ebuf != MAP_FAILED)\n+\t\tmunmap(self-\u003ebuf, BUF_SIZE * sizeof(uint64_t));\n+\tif (self-\u003efd \u003e= 0)\n+\t\tclose(self-\u003efd);\n+}\n+\n+TEST_F(kcov_dataflow, init_track)\n+{\n+\tint ret = ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE);\n+\n+\tASSERT_EQ(0, ret);\n+}\n+\n+TEST_F(kcov_dataflow, init_track_too_small)\n+{\n+\tint ret = ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, 1UL);\n+\n+\tASSERT_EQ(-1, ret);\n+\tASSERT_EQ(EINVAL, errno);\n+}\n+\n+TEST_F(kcov_dataflow, init_track_double)\n+{\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tASSERT_EQ(-1, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tASSERT_EQ(EBUSY, errno);\n+}\n+\n+TEST_F(kcov_dataflow, mmap_before_init)\n+{\n+\tself-\u003ebuf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+\t\t\t PROT_READ | PROT_WRITE, MAP_SHARED, self-\u003efd, 0);\n+\tASSERT_EQ(MAP_FAILED, self-\u003ebuf);\n+}\n+\n+TEST_F(kcov_dataflow, enable_disable)\n+{\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tself-\u003ebuf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+\t\t\t PROT_READ | PROT_WRITE, MAP_SHARED, self-\u003efd, 0);\n+\tASSERT_NE(MAP_FAILED, self-\u003ebuf);\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_ENABLE, 0));\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_DISABLE, 0));\n+}\n+\n+TEST_F(kcov_dataflow, enable_without_mmap)\n+{\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\t/* enable works even without mmap (mmap is optional for setup) */\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_ENABLE, 0));\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_DISABLE, 0));\n+}\n+\n+TEST_F(kcov_dataflow, disable_without_enable)\n+{\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tASSERT_EQ(-1, ioctl(self-\u003efd, KCOV_DF_DISABLE, 0));\n+\tASSERT_EQ(EINVAL, errno);\n+}\n+\n+TEST_F(kcov_dataflow, double_enable)\n+{\n+\tint fd2;\n+\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tself-\u003ebuf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+\t\t\t PROT_READ | PROT_WRITE, MAP_SHARED, self-\u003efd, 0);\n+\tASSERT_NE(MAP_FAILED, self-\u003ebuf);\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_ENABLE, 0));\n+\n+\t/* Second fd should fail to enable (task already active) */\n+\tfd2 = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\n+\tASSERT_GE(fd2, 0);\n+\tASSERT_EQ(0, ioctl(fd2, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tASSERT_EQ(-1, ioctl(fd2, KCOV_DF_ENABLE, 0));\n+\tASSERT_EQ(EBUSY, errno);\n+\tclose(fd2);\n+\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_DISABLE, 0));\n+}\n+\n+TEST_F(kcov_dataflow, records_captured)\n+{\n+\tuint64_t count;\n+\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_INIT_TRACK, (unsigned long)BUF_SIZE));\n+\tself-\u003ebuf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),\n+\t\t\t PROT_READ | PROT_WRITE, MAP_SHARED, self-\u003efd, 0);\n+\tASSERT_NE(MAP_FAILED, self-\u003ebuf);\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_ENABLE, 0));\n+\n+\t/* Trigger some kernel code in this task */\n+\tgetpid();\n+\n+\tASSERT_EQ(0, ioctl(self-\u003efd, KCOV_DF_DISABLE, 0));\n+\n+\tcount = self-\u003ebuf[0];\n+\t/*\n+\t * With INSTRUMENT_ALL, getpid() produces records; without it count may\n+\t * be 0. Whatever was written must parse: known types (CMP records are\n+\t * interleaved with CONFIG_KCOV_ENABLE_COMPARISONS=y), at least one value\n+\t * word each, and a walk that ends exactly at area[0] inside the buffer.\n+\t */\n+\tASSERT_LE(count, (uint64_t)BUF_SIZE - 1);\n+\tif (count \u003e 0) {\n+\t\tuint64_t pos = 1, end = 1 + count;\n+\t\tunsigned int nargs = 0;\n+\n+\t\twhile (pos + KCOV_DF_RECORD_HDR_WORDS \u003c= end) {\n+\t\t\tuint64_t hdr = self-\u003ebuf[pos];\n+\t\t\tunsigned int type = KCOV_DF_HDR_TYPE(hdr);\n+\t\t\tunsigned int nvals = KCOV_DF_HDR_NVALS(hdr);\n+\n+\t\t\tASSERT_TRUE(type == DF_TYPE_ENTRY || type == DF_TYPE_RET ||\n+\t\t\t\t    type == KCOV_DF_TYPE_CMP);\n+\t\t\tASSERT_GE(nvals, 1);\n+\t\t\tif (type != KCOV_DF_TYPE_CMP)\n+\t\t\t\tnargs++;\n+\t\t\tpos += KCOV_DF_RECORD_WORDS(nvals);\n+\t\t}\n+\t\tASSERT_EQ(end, pos);\n+\t\tASSERT_GT(nargs, 0);\n+\t}\n+}\n+\n+TEST_HARNESS_MAIN\n"
}

2/1 2026/09/03 13:17 agent patch-evaluator
0m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "KCOV",
    "KCOV_DATAFLOW_ARGS",
    "KCOV_DATAFLOW_RET",
    "KCOV_DATAFLOW_INSTRUMENT_ALL",
    "KCOV_DATAFLOW_NO_INLINE",
    "KCOV_ENABLE_COMPARISONS",
    "DEBUG_FS",
    "DEBUG_INFO"
  ],
  "FocusSymbols": [
    "kcov_df_ioctl",
    "kcov_df_mmap",
    "kcov_df_open",
    "kcov_df_close",
    "kcov_df_remote_start",
    "kcov_df_remote_stop",
    "kcov_df_trace_cmp",
    "__sanitizer_cov_trace_args",
    "__sanitizer_cov_trace_ret",
    "kcov_dataflow_task_exit",
    "kcov_dataflow_task_init",
    "kcov_df_scratch_shrink_scan",
    "kcov_df_scratch_shrink_count",
    "write_comp_data"
  ],
  "Reasoning": "The patch introduces KCOV-Dataflow, adding a new debugfs interface (/sys/kernel/debug/kcov_dataflow), ioctls (KCOV_DF_INIT_TRACK, KCOV_DF_ENABLE, KCOV_DF_DISABLE, KCOV_DF_REMOTE_ENABLE, KCOV_DF_REMOTE_DISABLE), mmap, remote kworker tracing, task lifecycle tracking in fork/exit, memory shrinker management, and comparison fan-out. This introduces significant new kernel code and state management in reachable subsystems that warrants fuzzing.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit add0fd47998ebfbe54b3752a7ef1aa9d7ee4fc05
Author: syz-cluster <triage@syzkaller.com>
Date:   Thu Sep 3 13:17:20 2026 +0000

    syz-cluster: applied patch under review

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 a9245d827ddb6..057f4e14ff46e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14077,7 +14077,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
diff --git a/include/linux/kcov.h b/include/linux/kcov.h
index 895b761b2db15..55e1405bc4bc4 100644
--- a/include/linux/kcov.h
+++ b/include/linux/kcov.h
@@ -3,6 +3,7 @@
 #define _LINUX_KCOV_H
 
 #include <linux/sched.h>
+#include <linux/jump_label.h>
 #include <uapi/linux/kcov.h>
 
 struct task_struct;
@@ -28,6 +29,14 @@ enum kcov_mode {
 void kcov_task_init(struct task_struct *t);
 void kcov_task_exit(struct task_struct *t);
 
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+void kcov_dataflow_task_init(struct task_struct *t);
+void kcov_dataflow_task_exit(struct task_struct *t);
+#else
+static inline void kcov_dataflow_task_init(struct task_struct *t) {}
+static inline void kcov_dataflow_task_exit(struct task_struct *t) {}
+#endif
+
 #define kcov_prepare_switch(t)			\
 do {						\
 	(t)->kcov_mode |= KCOV_IN_CTXSW;	\
@@ -43,6 +52,29 @@ void kcov_remote_start(u64 handle);
 void kcov_remote_stop(void);
 struct kcov_common_handle_id kcov_common_handle(void);
 
+/*
+ * Validate a remote handle: it must be a well-formed kcov_remote_handle()
+ * encoding, and each caller states which subsystem/instance combinations it
+ * accepts. Shared by KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE so both
+ * collectors take handles from the same partitioned namespace.
+ */
+static inline bool kcov_check_handle(u64 handle, bool common_valid,
+				     bool uncommon_valid, bool zero_valid)
+{
+	if (handle & ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))
+		return false;
+	switch (handle & KCOV_SUBSYSTEM_MASK) {
+	case KCOV_SUBSYSTEM_COMMON:
+		return (handle & KCOV_INSTANCE_MASK) ?
+			common_valid : zero_valid;
+	case KCOV_SUBSYSTEM_USB:
+		return uncommon_valid;
+	default:
+		return false;
+	}
+	return false;
+}
+
 static inline void kcov_remote_start_common(struct kcov_common_handle_id id)
 {
 	kcov_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));
@@ -107,4 +139,88 @@ static inline void kcov_remote_start_usb_softirq(u64 id) {}
 static inline void kcov_remote_stop_softirq(void) {}
 
 #endif /* CONFIG_KCOV */
+
+/*
+ * kcov_dataflow remote API. The collector is a separate object from mainline
+ * kcov and is only linked in when at least one of the two capture modes is
+ * configured (see kernel/Makefile), so gate the declarations the same way
+ * kcov_dataflow_task_init() above is gated; a caller that brackets a region for
+ * both collectors then still builds on a KCOV-only config.
+ */
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+void kcov_df_remote_start(u64 handle);
+void kcov_df_remote_stop(void);
+#else
+static inline void kcov_df_remote_start(u64 handle) {}
+static inline void kcov_df_remote_stop(void) {}
+#endif
+
+/*
+ * Handle-typed wrapper mirroring kcov_remote_start_common(), so a subsystem that
+ * already routes its mainline kcov remote sections by struct
+ * kcov_common_handle_id can open a dataflow section on the very same handle
+ * without knowing how it is encoded. The two collectors keep separate per-task
+ * state and separate handle tables, so a section of each may be nested around
+ * the same region; user space registers the identical handle value with
+ * KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE to collect both.
+ *
+ * Unlike kcov_remote_start(), the dataflow section may only be opened from
+ * sleepable task context: kcov_df_remote_start()/kcov_df_remote_stop() take a
+ * mutex and may allocate or free the worker's scratch area. Both are no-ops in
+ * softirq/hardirq context, so a softirq-bracketing call site collects no
+ * dataflow records rather than misbehaving. A call site that is only
+ * sometimes atomic (spinlock held, preemption or irqs disabled) must not use
+ * this wrapper; CONFIG_DEBUG_ATOMIC_SLEEP reports such a caller.
+ *
+ * Without CONFIG_KCOV the handle carries no value (see struct
+ * kcov_common_handle_id), and dataflow depends on KCOV, so this is a no-op.
+ */
+#ifdef CONFIG_KCOV
+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)
+{
+	kcov_df_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));
+}
+#else
+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)
+{
+}
+#endif
+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) && \
+	(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))
+/*
+ * CONFIG_KCOV_ENABLE_COMPARISONS provides ONE trace-cmp instrumentation shared by
+ * mainline kcov and kcov-dataflow. kcov.c's __sanitizer_cov_trace_cmp*() callbacks
+ * route each operand pair through kcov_trace_cmp() below, which fans it out:
+ * mainline kcov always sees it (write_comp_data() records only when the task is
+ * in KCOV_MODE_TRACE_CMP), and a task with a live dataflow session gets a copy in
+ * its dataflow buffer as well. The two collectors are independent fds with no
+ * cross-exclusion, so a task may collect for both at once, and a dataflow-side
+ * drop (inert context, full buffer) never costs mainline kcov a record. kcov.c
+ * never references the dataflow side, one cmp symbol feeds both collectors, and
+ * there is no separate df_cmp symbol or compiler change.
+ *
+ * The dataflow branch is gated by a static key so that, while no dataflow session
+ * is live, this whole-kernel hot path is a patched-out NOP that costs nothing on
+ * top of mainline write_comp_data() (kcov_df_cmp_key is inc'd on dataflow enable
+ * in kcov_dataflow.c).
+ */
+DECLARE_STATIC_KEY_FALSE(kcov_df_cmp_key);
+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);
+void kcov_df_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip);
+static inline notrace void
+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)
+{
+	write_comp_data(type, arg1, arg2, ip);			/* mainline kcov */
+	if (static_branch_unlikely(&kcov_df_cmp_key) && current->kcov_df_enabled)
+		kcov_df_trace_cmp(type, arg1, arg2, ip);	/* kcov-dataflow */
+}
+#elif defined(CONFIG_KCOV_ENABLE_COMPARISONS)
+/* Comparisons without a dataflow build: route straight to mainline kcov. */
+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);
+static inline notrace void
+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)
+{
+	write_comp_data(type, arg1, arg2, ip);
+}
+#endif
 #endif /* _LINUX_KCOV_H */
diff --git a/include/linux/sched.h b/include/linux/sched.h
index eb12ff4cea6c2..589aa57e19124 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1553,6 +1553,40 @@ struct task_struct {
 	/* KCOV sequence number: */
 	int				kcov_sequence;
 
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+	/*
+	 * KCOV dataflow per-task record sequence counter (24 bits used) plus,
+	 * in bit 31, the recursion guard held while a callback is running:
+	 */
+	u32				kcov_df_seq;
+
+	/* KCOV dataflow: separate buffer for trace-args/trace-ret */
+	unsigned int			kcov_df_size;
+	void				*kcov_df_area;
+	bool				kcov_df_enabled;
+
+	/*
+	 * The kcov_dataflow object this task's session belongs to, NULL when
+	 * no session is active. The task holds a reference on it for the whole
+	 * session, whether local (KCOV_DF_ENABLE, mirrors t->kcov) or remote
+	 * (kcov_df_remote_start()), so the buffer can never be freed under an
+	 * instrumented callback and both task exit and kcov_df_remote_stop()
+	 * reach the exact object without a hash lookup.
+	 */
+	struct kcov_dataflow		*kcov_df;
+
+	/*
+	 * Nesting depth of kcov_df_remote_start() on this task: 0 while no
+	 * remote session is active (including during a local session), 1 for
+	 * a normal bracketed work item. If a buggy caller nests, the inner
+	 * start()s only bump this and the inner stop()s only decrement it, so
+	 * the OUTER session (buffer + ref) is torn down exactly once, at the
+	 * outermost stop -- never early, which would otherwise drop the ref
+	 * and free the buffer out from under the still-running outer worker.
+	 */
+	int				kcov_df_remote_depth;
+#endif
+
 	/* Collect coverage from softirq context: */
 	unsigned int			kcov_softirq;
 
diff --git a/include/uapi/linux/kcov_dataflow.h b/include/uapi/linux/kcov_dataflow.h
new file mode 100644
index 0000000000000..db3112a45832c
--- /dev/null
+++ b/include/uapi/linux/kcov_dataflow.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_KCOV_DATAFLOW_H
+#define _LINUX_KCOV_DATAFLOW_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+/*
+ * User space ABI of /sys/kernel/debug/kcov_dataflow, see
+ * Documentation/dev-tools/kcov-dataflow.rst.
+ *
+ * KCOV_DF_INIT_TRACK takes the buffer size in u64 words by value (same
+ * convention as KCOV_INIT_TRACE). KCOV_DF_REMOTE_ENABLE takes a pointer to a
+ * __u64 remote handle encoded with kcov_remote_handle() (linux/kcov.h), so the
+ * full 64-bit value survives 32-bit and compat callers.
+ */
+#define KCOV_DF_INIT_TRACK	_IOR('d', 1, unsigned long)
+#define KCOV_DF_ENABLE		_IO('d', 100)
+#define KCOV_DF_DISABLE		_IO('d', 101)
+#define KCOV_DF_REMOTE_ENABLE	_IOW('d', 102, __u64)
+#define KCOV_DF_REMOTE_DISABLE	_IO('d', 103)
+
+/*
+ * Buffer layout (all u64 words):
+ *
+ *   area[0]                number of record words written after area[0]
+ *   area[1 + n ..]         records, back to back, each:
+ *
+ *     [0] header           see KCOV_DF_HDR_* below
+ *     [1] pc               instrumented location; KASLR offset removed, like
+ *                          the PCs mainline kcov records
+ *     [2] ENTRY/RET: the traced value's address (full pointer); may be a
+ *                    NULL/ERR_PTR value the callee received, in which case the
+ *                    value words hold KCOV_DF_MAGIC_BAD
+ *         CMP:       comparison type, KCOV_CMP_SIZE()/KCOV_CMP_CONST bits
+ *                    (linux/kcov.h)
+ *     [3 .. 3 + nvals)     value words: the scalar (nvals == 1), the expanded
+ *                          struct fields, or the two CMP operands (nvals == 2)
+ *
+ * The header packs:
+ *
+ *   bits  0..23  per-task record sequence number
+ *   bits 28..31  record type, KCOV_DF_TYPE_*
+ *   bits 32..47  nvals, the number of value words that follow word [2]
+ *   bits 48..55  ENTRY/RET: size in bytes of the traced argument/return value
+ *                (clamped to 255)
+ *   bits 56..63  ENTRY: argument index (clamped to 255); RET: 0
+ *
+ * A consumer walks the buffer as
+ *
+ *	pos = 1;
+ *	while (pos < 1 + area[0]) {
+ *		hdr = area[pos];
+ *		nvals = KCOV_DF_HDR_NVALS(hdr);
+ *		...
+ *		pos += KCOV_DF_RECORD_WORDS(nvals);
+ *	}
+ *
+ * area[0] never exceeds the buffer size minus one, and every counted word has
+ * been written, so the walk above stays inside the mapping.
+ */
+#define KCOV_DF_TYPE_CMP	0xC
+#define KCOV_DF_TYPE_ENTRY	0xE
+#define KCOV_DF_TYPE_RET	0xF
+
+#define KCOV_DF_HDR_SEQ_MASK	0x00FFFFFFULL
+#define KCOV_DF_HDR_TYPE_SHIFT	28
+#define KCOV_DF_HDR_TYPE_MASK	0xFULL
+#define KCOV_DF_HDR_NVALS_SHIFT	32
+#define KCOV_DF_HDR_NVALS_MASK	0xFFFFULL
+#define KCOV_DF_HDR_SIZE_SHIFT	48
+#define KCOV_DF_HDR_SIZE_MASK	0xFFULL
+#define KCOV_DF_HDR_ARGIDX_SHIFT 56
+#define KCOV_DF_HDR_ARGIDX_MASK	0xFFULL
+
+#define KCOV_DF_HDR_SEQ(h)	((h) & KCOV_DF_HDR_SEQ_MASK)
+#define KCOV_DF_HDR_TYPE(h)	(((h) >> KCOV_DF_HDR_TYPE_SHIFT) & KCOV_DF_HDR_TYPE_MASK)
+#define KCOV_DF_HDR_NVALS(h)	(((h) >> KCOV_DF_HDR_NVALS_SHIFT) & KCOV_DF_HDR_NVALS_MASK)
+#define KCOV_DF_HDR_SIZE(h)	(((h) >> KCOV_DF_HDR_SIZE_SHIFT) & KCOV_DF_HDR_SIZE_MASK)
+#define KCOV_DF_HDR_ARGIDX(h)	(((h) >> KCOV_DF_HDR_ARGIDX_SHIFT) & KCOV_DF_HDR_ARGIDX_MASK)
+
+/* Words per record: header, pc, pointer/cmp-type, then the value words. */
+#define KCOV_DF_RECORD_HDR_WORDS	3
+#define KCOV_DF_RECORD_WORDS(nvals)	(KCOV_DF_RECORD_HDR_WORDS + (nvals))
+
+/* Largest nvals a record can carry; longer field lists are truncated. */
+#define KCOV_DF_MAX_VALS	KCOV_DF_HDR_NVALS_MASK
+
+/* Value word written when the traced pointer or a field could not be read. */
+#define KCOV_DF_MAGIC_BAD	0xBADADD85ULL
+
+#endif /* _LINUX_KCOV_DATAFLOW_H */
diff --git a/kernel/Makefile b/kernel/Makefile
index 1e1a31673577d..307b7fd1e1f96 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -44,6 +44,12 @@ KCSAN_SANITIZE_kcov.o := n
 UBSAN_SANITIZE_kcov.o := n
 KMSAN_SANITIZE_kcov.o := n
 
+KCOV_INSTRUMENT_kcov_dataflow.o := n
+KASAN_SANITIZE_kcov_dataflow.o := n
+KCSAN_SANITIZE_kcov_dataflow.o := n
+UBSAN_SANITIZE_kcov_dataflow.o := n
+KMSAN_SANITIZE_kcov_dataflow.o := n
+
 CONTEXT_ANALYSIS_kcov.o := y
 CFLAGS_kcov.o := $(call cc-option, -fno-conserve-stack) -fno-stack-protector
 
@@ -98,6 +104,9 @@ obj-$(CONFIG_AUDIT) += audit.o auditfilter.o
 obj-$(CONFIG_AUDITSYSCALL) += auditsc.o audit_watch.o audit_fsnotify.o audit_tree.o
 obj-$(CONFIG_GCOV_KERNEL) += gcov/
 obj-$(CONFIG_KCOV) += kcov.o
+ifneq ($(CONFIG_KCOV_DATAFLOW_ARGS)$(CONFIG_KCOV_DATAFLOW_RET),)
+obj-y += kcov_dataflow.o
+endif
 obj-$(CONFIG_KPROBES) += kprobes.o
 obj-$(CONFIG_FAIL_FUNCTION) += fail_function.o
 obj-$(CONFIG_KGDB) += debug/
diff --git a/kernel/exit.c b/kernel/exit.c
index 97686af895013..8881661d635ba 100644
--- a/kernel/exit.c
+++ b/kernel/exit.c
@@ -939,6 +939,7 @@ void __noreturn do_exit(long code)
 		kthread_do_exit(kthread, code);
 
 	kcov_task_exit(tsk);
+	kcov_dataflow_task_exit(tsk);
 	kmsan_task_exit(tsk);
 
 	synchronize_group_exit(tsk, code);
diff --git a/kernel/fork.c b/kernel/fork.c
index 22283bf849e15..14d4fe5c7909b 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -985,6 +985,7 @@ static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
 	tsk->worker_private = NULL;
 
 	kcov_task_init(tsk);
+	kcov_dataflow_task_init(tsk);
 	kmsan_task_create(tsk);
 	kmap_local_fork(tsk);
 
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 35420f0ac524d..cac9b69e197ed 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -232,7 +232,14 @@ void notrace __sanitizer_cov_trace_pc(void)
 EXPORT_SYMBOL(__sanitizer_cov_trace_pc);
 
 #ifdef CONFIG_KCOV_ENABLE_COMPARISONS
-static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
+/*
+ * Mainline kcov comparison writer: appends to the task's own kcov buffer, and
+ * only in KCOV_MODE_TRACE_CMP. The fan-out that also feeds the kcov-dataflow
+ * buffer lives in kcov_trace_cmp() in <linux/kcov.h>, so kcov.c never references
+ * the dataflow side itself. This writer is only non-static so that header helper
+ * (which the cmp callbacks below call) can reach it.
+ */
+void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
 {
 	struct task_struct *t;
 	u64 *area;
@@ -267,55 +274,59 @@ static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
 	}
 }
 
+/*
+ * The __sanitizer_cov_trace_cmp*() callbacks stay here in kcov.c (one shared,
+ * compiler-emitted symbol per comparison -- no separate df_cmp symbol, no
+ * compiler change). Each routes its operand pair through kcov_trace_cmp()
+ * (defined in <linux/kcov.h>), which records into mainline kcov and, when this
+ * task has a dataflow session, into kcov-dataflow too. kcov.c never names the
+ * dataflow side; that fan-out lives entirely in the header.
+ */
 void notrace __sanitizer_cov_trace_cmp1(u8 arg1, u8 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp1);
 
 void notrace __sanitizer_cov_trace_cmp2(u16 arg1, u16 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp2);
 
 void notrace __sanitizer_cov_trace_cmp4(u32 arg1, u32 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp4);
 
 void notrace __sanitizer_cov_trace_cmp8(kcov_u64 arg1, kcov_u64 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp8);
 
 void notrace __sanitizer_cov_trace_const_cmp1(u8 arg1, u8 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp1);
 
 void notrace __sanitizer_cov_trace_const_cmp2(u16 arg1, u16 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp2);
 
 void notrace __sanitizer_cov_trace_const_cmp4(u32 arg1, u32 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp4);
 
 void notrace __sanitizer_cov_trace_const_cmp8(kcov_u64 arg1, kcov_u64 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp8);
 
@@ -344,7 +355,7 @@ void notrace __sanitizer_cov_trace_switch(kcov_u64 val, void *arg)
 		return;
 	}
 	for (i = 0; i < count; i++)
-		write_comp_data(type, cases[i + 2], val, _RET_IP_);
+		kcov_trace_cmp(type, cases[i + 2], val, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_switch);
 #endif /* ifdef CONFIG_KCOV_ENABLE_COMPARISONS */
@@ -587,23 +598,6 @@ static void kcov_fault_in_area(struct kcov *kcov)
 		READ_ONCE(area[offset]);
 }
 
-static inline bool kcov_check_handle(u64 handle, bool common_valid,
-				bool uncommon_valid, bool zero_valid)
-{
-	if (handle & ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))
-		return false;
-	switch (handle & KCOV_SUBSYSTEM_MASK) {
-	case KCOV_SUBSYSTEM_COMMON:
-		return (handle & KCOV_INSTANCE_MASK) ?
-			common_valid : zero_valid;
-	case KCOV_SUBSYSTEM_USB:
-		return uncommon_valid;
-	default:
-		return false;
-	}
-	return false;
-}
-
 static int kcov_ioctl_locked(struct kcov *kcov, unsigned int cmd,
 			     unsigned long arg)
 	__must_hold(&kcov->lock)
diff --git a/kernel/kcov_dataflow.c b/kernel/kcov_dataflow.c
new file mode 100644
index 0000000000000..641d6bc763864
--- /dev/null
+++ b/kernel/kcov_dataflow.c
@@ -0,0 +1,1193 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KCOV Dataflow: per-task function argument/return value capture.
+ *
+ * Exposes /sys/kernel/debug/kcov_dataflow, completely independent from
+ * /sys/kernel/debug/kcov. Own buffer, own ioctl, own mmap.
+ *
+ * The user-visible ABI:
+ *
+ * ioctls, the record layout and the header bit fields, is defined in
+ * <uapi/linux/kcov_dataflow.h>. In short, every record is
+ *
+ *   [hdr][pc][ptr or cmp type][nvals value words]
+ *
+ * appended after area[0], which counts the record words written so far.
+ */
+#define pr_fmt(fmt) "kcov_dataflow: " fmt
+
+#define DISABLE_BRANCH_PROFILING
+#include <linux/atomic.h>
+#include <linux/bits.h>
+#include <linux/compiler.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/types.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/preempt.h>
+#include <linux/refcount.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/shrinker.h>
+#include <linux/mutex.h>
+#include <linux/hashtable.h>
+#include <linux/vmalloc.h>
+#include <linux/debugfs.h>
+#include <linux/uaccess.h>
+#include <linux/jump_label.h>
+#include <linux/kcov.h>
+#include <uapi/linux/kcov_dataflow.h>
+#include <asm/setup.h>
+
+/*
+ * Comparison capture is shared with mainline kcov; it only exists when both the
+ * trace-cmp instrumentation and the dataflow task state are configured in.
+ */
+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) && \
+	(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))
+#define KCOV_DF_HAVE_CMP 1
+#endif
+
+#define KCOV_DF_IS_ERR(p)	((unsigned long)(p) >= (unsigned long)-4095UL)
+
+/*
+ * Bit 31 of task_struct::kcov_df_seq is the per-task recursion guard, held
+ * while one of the callbacks below runs. The record sequence number lives in
+ * the low 24 bits (KCOV_DF_HDR_SEQ_MASK) and is advanced with kcov_df_next_seq()
+ * so that it wraps inside its own field and can never carry into the guard.
+ */
+#define KCOV_DF_SEQ_GUARD	BIT(31)
+
+/*
+ * Per-worker private scratch size (u64 words), KCOV's remote-area model: a
+ * remote kworker collects into its OWN scratch and merges it into the shared
+ * ->area at kcov_df_remote_stop(). Fixed and small (8 MiB) -- one work item's
+ * coverage, not a whole buffer -- so the pool of recycled scratch areas stays
+ * bounded regardless of how many kworkers churn. Overflowing a scratch just
+ * drops that worker's excess records (same as a full buffer), never corrupts.
+ */
+#define KCOV_DF_REMOTE_WORDS	(1UL << 20)
+
+struct kcov_dataflow {
+	struct mutex	lock;
+	unsigned int	size;	/* in u64 words */
+	void		*area;
+	/*
+	 * Task with a local (KCOV_DF_ENABLE) session on this object, NULL if
+	 * none. Mirrors struct kcov::t: that task holds its own reference (see
+	 * ->refcount) and points back at us through task_struct::kcov_df, so
+	 * KCOV_DF_DISABLE, close() and task exit all unwire the same session.
+	 */
+	struct task_struct *t;
+	/*
+	 * Lifetime refcount (KCOV's struct kcov pattern). The open fd holds one
+	 * ref; the task enabled with KCOV_DF_ENABLE holds one for as long as its
+	 * session lasts (dropped by KCOV_DF_DISABLE, by close() from that task,
+	 * or by task exit -- it cannot be unwired from another task); each
+	 * kcov_df_remote_start() takes one and the matching kcov_df_remote_stop()
+	 * drops it. Whoever drops the LAST ref frees ->area and the object
+	 * (kcov_df_put), so an instrumented callback can never write through a
+	 * freed buffer, whichever task does the final close().
+	 */
+	refcount_t	refcount;
+	u64		remote_handle; /* handle for remote lookup, 0 if not published */
+#ifdef KCOV_DF_HAVE_CMP
+	/*
+	 * Whether this fd holds a ref on kcov_df_cmp_key, tracked SEPARATELY for
+	 * the local (KCOV_DF_ENABLE) and remote (KCOV_DF_REMOTE_ENABLE) sources.
+	 * A single shared flag let a KCOV_DF_DISABLE drop the key while a remote
+	 * handle was still published -- silently losing the live remote workers'
+	 * comparison records. Two flags mean releasing one source never pulls the
+	 * key out from under the other. Both are only touched under ->lock.
+	 */
+	bool		cmp_key_local;
+	bool		cmp_key_remote;
+#endif
+};
+
+/* Which activation source holds the cmp static key (see kcov_df_cmp_key_hold). */
+enum { KCOV_DF_CMP_LOCAL, KCOV_DF_CMP_REMOTE };
+
+#ifdef KCOV_DF_HAVE_CMP
+/*
+ * Static key gating the per-comparison dataflow check in kcov_trace_cmp()
+ * (linux/kcov.h). It is a patched-out NOP until at least one dataflow session is
+ * live, so trace-cmp across the WHOLE kernel costs nothing extra while no
+ * dataflow fuzzing runs; only an active session flips it on. Refcounted: inc on
+ * each source's first enable, dec on its disable/close/exit (idempotent,
+ * tracked per source via cmp_key_local / cmp_key_remote so releasing one never
+ * drops the key from under the other).
+ *
+ * The key is only ever inc'd/dec'd from ioctl, close() and do_exit() context,
+ * under df->lock -- never from kcov_df_remote_stop() or the last kcov_df_put(),
+ * so a subsystem's worker path never ends up under cpus_read_lock() and
+ * jump_label_mutex. The static_branch_{inc,dec}() text-patch is amortised -- it
+ * fires only on the 0->1 and 1->0 transitions, not per fd while sessions overlap.
+ */
+DEFINE_STATIC_KEY_FALSE(kcov_df_cmp_key);
+EXPORT_SYMBOL(kcov_df_cmp_key);
+
+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which)
+{
+	bool *held = which == KCOV_DF_CMP_LOCAL ? &df->cmp_key_local
+						: &df->cmp_key_remote;
+
+	lockdep_assert_held(&df->lock);
+	if (!*held) {
+		*held = true;
+		static_branch_inc(&kcov_df_cmp_key);
+	}
+}
+
+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which)
+{
+	bool *held = which == KCOV_DF_CMP_LOCAL ? &df->cmp_key_local
+						: &df->cmp_key_remote;
+
+	lockdep_assert_held(&df->lock);
+	if (*held) {
+		*held = false;
+		static_branch_dec(&kcov_df_cmp_key);
+	}
+}
+
+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df)
+{
+	return df->cmp_key_local || df->cmp_key_remote;
+}
+#else
+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which) {}
+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which) {}
+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df) { return false; }
+#endif
+
+/* Remote dataflow: handle-based lookup (follows KCOV's kcov_remote_map pattern) */
+static DEFINE_MUTEX(kcov_df_remote_lock);
+static DEFINE_HASHTABLE(kcov_df_remote_map, 4);
+
+struct kcov_df_remote {
+	u64			handle;
+	struct kcov_dataflow	*df;
+	struct hlist_node	hnode;
+};
+
+static struct kcov_df_remote *kcov_df_remote_find(u64 handle)
+{
+	struct kcov_df_remote *remote;
+
+	hash_for_each_possible(kcov_df_remote_map, remote, hnode, handle) {
+		if (remote->handle == handle)
+			return remote;
+	}
+	return NULL;
+}
+
+/* Unpublish @df's remote handle, if any; no new remote session can start. */
+static void kcov_df_remote_unpublish(struct kcov_dataflow *df)
+{
+	struct kcov_df_remote *remote;
+
+	mutex_lock(&kcov_df_remote_lock);
+	if (df->remote_handle) {
+		remote = kcov_df_remote_find(df->remote_handle);
+		if (remote) {
+			hash_del(&remote->hnode);
+			kfree(remote);
+		}
+		df->remote_handle = 0;
+	}
+	mutex_unlock(&kcov_df_remote_lock);
+}
+
+static void kcov_df_get(struct kcov_dataflow *df)
+{
+	refcount_inc(&df->refcount);
+}
+
+/*
+ * Drop a reference; the last one frees the buffer and the object. Only called
+ * from sleepable task context (ioctl, close(), do_exit(), and remote_stop()
+ * which requires it), so vfree() here is fine. No caller may touch @df after
+ * its own kcov_df_put(). Every path that unwires a session releases its cmp
+ * key ref under df->lock first, so nothing is left to balance here.
+ */
+static void kcov_df_put(struct kcov_dataflow *df)
+{
+	if (refcount_dec_and_test(&df->refcount)) {
+		WARN_ON_ONCE(kcov_df_cmp_key_held(df));
+		vfree(df->area);
+		kfree(df);
+	}
+}
+
+/*
+ * Touch every page of a buffer before a task starts collecting into it, the
+ * same way kcov_fault_in_area() does for KCOV_ENABLE: on configurations with
+ * lazily populated vmalloc mappings the first access would otherwise fault
+ * from inside an instrumented callback, and code on the vmalloc fault path may
+ * itself be instrumented.
+ */
+static void kcov_df_fault_in_area(u64 *area, unsigned long size)
+{
+	unsigned long stride = PAGE_SIZE / sizeof(u64);
+	unsigned long off;
+
+	for (off = 0; off < size; off += stride)
+		READ_ONCE(area[off]);
+}
+
+/*
+ * Pool of recycled per-worker scratch areas (KCOV's kcov_remote_areas). All are
+ * KCOV_DF_REMOTE_WORDS u64s. While parked on the freelist the area's first bytes
+ * hold this list_head; while in use word[0] is the scratch write cursor. Guarded
+ * by kcov_df_remote_lock.
+ */
+struct kcov_df_scratch {
+	struct list_head list;
+};
+static LIST_HEAD(kcov_df_scratch_pool);
+static unsigned long kcov_df_scratch_pool_nr;	/* idle areas parked in the pool */
+
+/* Take a scratch area from the pool, or NULL if empty (caller vmalloc()s one). */
+static void *kcov_df_scratch_get(void)
+{
+	struct kcov_df_scratch *s;
+
+	if (list_empty(&kcov_df_scratch_pool))
+		return NULL;
+	s = list_first_entry(&kcov_df_scratch_pool, struct kcov_df_scratch, list);
+	list_del(&s->list);
+	kcov_df_scratch_pool_nr--;
+	return s;
+}
+
+/* Return a scratch area to the pool for reuse. */
+static void kcov_df_scratch_put(void *area)
+{
+	struct kcov_df_scratch *s = area;
+
+	INIT_LIST_HEAD(&s->list);
+	list_add(&s->list, &kcov_df_scratch_pool);
+	kcov_df_scratch_pool_nr++;
+}
+
+/*
+ * Merge a remote worker's private scratch into the shared ->area, appending its
+ * records at the shared write cursor. This is the ONE many-writers path (several
+ * kworkers merge concurrently), so it claims its region with a cmpxchg loop on
+ * area[0]: the bounds are checked against the value about to be committed, and
+ * the commit only happens when the record fits. area[0] therefore never exceeds
+ * the buffer capacity and every counted word has been written, so a consumer
+ * walking area[0] words stays inside its mapping. A concurrent reset by user
+ * space (writing area[0] = 0 to restart collection) simply makes the cmpxchg
+ * fail and the loop re-read the new cursor; there is no subtract, so the
+ * counter can never go negative or wrap past the bounds check. Each merge claims
+ * a disjoint [start, start+n), so concurrent merges don't overlap and need no
+ * lock. @df is kept alive by the caller's reference, so ->area is stable here.
+ *
+ * ->area is never written through kcov_df_reserve() while a remote handle is
+ * published (KCOV_DF_ENABLE refuses that), so this atomic cursor update never
+ * races a plain read-modify-write of the same word.
+ */
+static void kcov_df_merge(struct kcov_dataflow *df, const u64 *scratch)
+{
+	u64 *area = df->area;
+	atomic64_t *cursor;
+	u64 n, count, capacity;
+	s64 old;
+
+	if (!area)
+		return;
+	/*
+	 * scratch[0] is an EXACT high-water of written words: kcov_df_reserve()
+	 * commits the count only after a record fits, so every counted word was
+	 * really written -- the merge never publishes the unwritten
+	 * (recycled/uninitialized) tail of a pooled scratch. The clamp below is thus
+	 * belt-and-suspenders against a stray count.
+	 */
+	n = scratch[0];
+	if (n > KCOV_DF_REMOTE_WORDS - 1)
+		n = KCOV_DF_REMOTE_WORDS - 1;
+	if (!n)
+		return;
+
+	capacity = df->size - 1;	/* words after area[0] */
+	cursor = (atomic64_t *)&area[0];
+	old = atomic64_read(cursor);
+	do {
+		count = old;
+		/* Full (or a garbage cursor from user space): drop the records. */
+		if (count > capacity || n > capacity - count)
+			return;
+	} while (!atomic64_try_cmpxchg(cursor, &old, count + n));
+	memcpy(&area[1 + count], &scratch[1], n * sizeof(u64));
+}
+
+/*
+ * Reserve @record_len u64 words in the current task's buffer. On success return
+ * true and store the 1-based start index of the record's data region.
+ *
+ * Single-writer discipline, identical to mainline kcov.c: the current task is the
+ * ONLY instrumented writer of @area. In remote mode @area is this kworker's OWN
+ * private scratch; in local (KCOV_DF_ENABLE) mode it is the enabling task's own
+ * mmapped buffer -- and only one task can hold that (the KCOV_DF_ENABLE EBUSY
+ * guard, which also refuses a buffer with a published remote handle, so
+ * kcov_df_merge() never touches this word concurrently). Two tasks never write
+ * the same @area here, so no atomic is needed: validate FIRST and commit the
+ * count (area[0]) only on success, so area[0] is always an EXACT high-water of
+ * written words and no consumer (userspace or kcov_df_merge()) ever sees an
+ * unwritten/recycled slot.
+ *
+ * (Publishing a worker's scratch into the shared ->area is the SEPARATE
+ * kcov_df_merge() path, which DOES reserve atomically because many kworkers merge
+ * concurrently.)
+ *
+ * READ_ONCE/WRITE_ONCE because in local mode userspace may reset area[0] to 0
+ * between operations. That reset can only drive the count to 0, never negative
+ * (there is no subtract), so a racing reset may drop records but can never produce
+ * an out-of-bounds store. This is exactly mainline kcov's contract.
+ *
+ * __always_inline because kcov_df_trace_cmp() below is on objtool's
+ * uaccess_safe_builtin[] list, and objtool rejects any out-of-line call made
+ * from such a function; do not leave that to the optimizer.
+ */
+static __always_inline notrace __no_sanitize_coverage bool
+kcov_df_reserve(struct task_struct *t, u64 *area, u32 record_len,
+		unsigned long *start_index)
+{
+	unsigned long count = READ_ONCE(area[0]);
+
+	*start_index = 1 + count;
+	if (count >= t->kcov_df_size ||
+	    record_len > t->kcov_df_size - *start_index)
+		return false;
+	WRITE_ONCE(area[0], count + record_len);
+	return true;
+}
+
+/*
+ * Contexts where dataflow collection must stay completely inert.
+ *
+ * Beyond the obvious !in_task() case, this bails whenever page faults are
+ * disabled. copy_from_kernel_nofault() -- used by kcov_df_write() below to read
+ * traced pointers, and, crucially, by the ORC stack unwinder that KASAN runs on
+ * every slab free (set_track_prepare() -> stack_trace_save()) -- brackets its
+ * raw loads with pagefault_disable(), and those loads carry trace-cmp/trace-args
+ * instrumentation. Without this bail a single stack walk under a fuzzing + KASAN
+ * workload floods the collector with a callback per load and soft-locks the CPU.
+ *
+ * pagefault_disabled() is true throughout any such nofault region no matter
+ * which instrumented leaf issued the callback, so testing it here contains the
+ * whole class of self-instrumentation storms -- the bit-31 recursion guard below
+ * only covers re-entry nested inside our own callback, not a fresh entry from
+ * the unwinder/KASAN path. Contained entirely to this file: no coverage
+ * exclusion in mm/ or arch/ is needed.
+ *
+ * The trade-off is that records are also dropped inside unrelated
+ * pagefault_disable() regions (kmap_atomic() on HIGHMEM, futex and perf
+ * callchain probes, ...). Those are short and rare on the fuzzing workloads this
+ * targets; a per-task "in nofault region" flag would remove the coupling at the
+ * cost of touching mm/maccess.c.
+ */
+static __always_inline notrace __no_sanitize_coverage bool
+kcov_df_inert_context(void)
+{
+	return !in_task() || pagefault_disabled();
+}
+
+/* Same as kcov.c: record PCs with the KASLR offset removed. */
+static __always_inline notrace __no_sanitize_coverage u64
+kcov_df_canonicalize_ip(u64 ip)
+{
+#ifdef CONFIG_RANDOMIZE_BASE
+	ip -= kaslr_offset();
+#endif
+	return ip;
+}
+
+/*
+ * Advance the task's 24-bit record sequence number, keeping the guard bit set.
+ * Masking the increment keeps the counter from ever carrying into
+ * KCOV_DF_SEQ_GUARD, which would reopen re-entry in the middle of a record.
+ */
+static __always_inline notrace __no_sanitize_coverage u32
+kcov_df_next_seq(struct task_struct *t)
+{
+	u32 seq = (t->kcov_df_seq + 1) & KCOV_DF_HDR_SEQ_MASK;
+
+	t->kcov_df_seq = KCOV_DF_SEQ_GUARD | seq;
+	return seq;
+}
+
+static __always_inline notrace __no_sanitize_coverage u64
+kcov_df_hdr(u64 type, u32 nvals, u32 size, u32 arg_idx, u32 seq)
+{
+	return (type << KCOV_DF_HDR_TYPE_SHIFT) |
+	       ((u64)nvals << KCOV_DF_HDR_NVALS_SHIFT) |
+	       ((u64)min_t(u32, size, KCOV_DF_HDR_SIZE_MASK) <<
+		KCOV_DF_HDR_SIZE_SHIFT) |
+	       ((u64)min_t(u32, arg_idx, KCOV_DF_HDR_ARGIDX_MASK) <<
+		KCOV_DF_HDR_ARGIDX_SHIFT) |
+	       (seq & KCOV_DF_HDR_SEQ_MASK);
+}
+
+/*
+ * Core write function for ENTRY/RET records.
+ * Uses the same READ_ONCE/WRITE_ONCE pattern as write_comp_data() in kcov.c.
+ *
+ * @num_fields is the length of the compiler-supplied @offsets table (pairs of
+ * offset,size) for an expanded struct, 0 for a scalar read directly from @ptr
+ * with width @size. It is clamped to KCOV_DF_MAX_VALS so the record length can
+ * never wrap and the field loop is bounded by the words actually reserved.
+ */
+static noinline notrace __no_sanitize_coverage void
+kcov_df_write(u64 type, u64 pc, u32 arg_idx, u32 size, void *ptr,
+	      u64 *offsets, u32 num_fields)
+{
+	struct task_struct *t = current;
+	u64 *area;
+	unsigned long start_index;
+	u32 nvals, seq, i;
+
+	if (kcov_df_inert_context())
+		return;
+
+	if (!t->kcov_df_enabled)
+		return;
+
+	/*
+	 * Prevent recursion: functions called by this callback
+	 * (copy_from_kernel_nofault) may be instrumented. Use the
+	 * sequence counter's high bit as a per-task guard.
+	 */
+	if (t->kcov_df_seq & KCOV_DF_SEQ_GUARD)
+		return;
+	t->kcov_df_seq |= KCOV_DF_SEQ_GUARD;
+	/* Paired with the barrier() before the guard is cleared at out:. */
+	barrier();
+
+	area = (u64 *)t->kcov_df_area;
+	if (!area)
+		goto out;
+
+	if (num_fields > KCOV_DF_MAX_VALS)
+		num_fields = KCOV_DF_MAX_VALS;
+	/* Record: header + pc + ptr, then the fields or one scalar word. */
+	nvals = num_fields > 0 ? num_fields : 1;
+
+	if (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(nvals), &start_index))
+		goto out;
+
+	seq = kcov_df_next_seq(t);
+	area[start_index] = kcov_df_hdr(type, nvals, size, arg_idx, seq);
+	area[start_index + 1] = kcov_df_canonicalize_ip(pc);
+	area[start_index + 2] = (u64)(unsigned long)ptr;
+
+	if (num_fields == 0) {
+		u64 val = 0;
+		u32 sz = size;
+
+		/*
+		 * Read the scalar with a compile-time-constant width for the
+		 * common sizes so the compiler folds away copy_from_kernel_
+		 * nofault()'s runtime size loop and alignment branching; fall
+		 * back to the variable-size byte copy for anything else. A
+		 * faulting read leaves val == 0, matching the prior best-effort
+		 * behaviour.
+		 */
+		if (ptr && !KCOV_DF_IS_ERR(ptr)) {
+			switch (sz) {
+			case 8: {
+				u64 v = 0;
+
+				if (!get_kernel_nofault(v, (u64 *)ptr))
+					val = v;
+				break;
+			}
+			case 4: {
+				u32 v = 0;
+
+				if (!get_kernel_nofault(v, (u32 *)ptr))
+					val = v;
+				break;
+			}
+			case 2: {
+				u16 v = 0;
+
+				if (!get_kernel_nofault(v, (u16 *)ptr))
+					val = v;
+				break;
+			}
+			case 1: {
+				u8 v = 0;
+
+				if (!get_kernel_nofault(v, (u8 *)ptr))
+					val = v;
+				break;
+			}
+			default:
+				if (sz > sizeof(val))
+					sz = sizeof(val);
+				copy_from_kernel_nofault(&val, ptr, sz);
+			}
+		}
+		area[start_index + 3] = val;
+	} else {
+		if (!ptr || KCOV_DF_IS_ERR(ptr)) {
+			for (i = 0; i < num_fields; i++)
+				area[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;
+			goto out;
+		}
+		for (i = 0; i < num_fields; i++) {
+			u64 off, sz, val = KCOV_DF_MAGIC_BAD;
+			void *fa;
+
+			if (copy_from_kernel_nofault(&off, &offsets[i * 2], sizeof(off)) ||
+			    copy_from_kernel_nofault(&sz, &offsets[i * 2 + 1], sizeof(sz))) {
+				area[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;
+				continue;
+			}
+			fa = (void *)((unsigned long)ptr + off);
+			val = 0;
+
+			if (sz <= sizeof(val)) {
+				if (copy_from_kernel_nofault(&val, fa, sz))
+					val = KCOV_DF_MAGIC_BAD;
+			} else {
+				if (copy_from_kernel_nofault(&val, fa, sizeof(val)))
+					val = KCOV_DF_MAGIC_BAD;
+			}
+			area[start_index + 3 + i] = val;
+		}
+	}
+out:
+	/*
+	 * Paired with the barrier() after setting the guard at the top.
+	 * Ensures all record writes are complete before we clear the
+	 * recursion guard.
+	 */
+	barrier();
+	t->kcov_df_seq &= ~KCOV_DF_SEQ_GUARD;
+}
+
+/*
+ * The two compiler-emitted entry points are on objtool's uaccess_safe_builtin[]
+ * list, like the __sanitizer_cov_trace_cmp*() callbacks. The trace-args call is
+ * planted before the terminator of the function's entry block (so that every
+ * spilled value dominates it), not at its first instruction: a function that
+ * opens a user access region and then does an unsafe_get_user() -- an asm goto,
+ * hence a block terminator -- gets the callback AFTER the stac, and objtool
+ * reports "call to __sanitizer_cov_trace_args() with UACCESS enabled".
+ *
+ * objtool validates a listed function with AC set and rejects any out-of-line
+ * call from it, and kcov_df_write() calls copy_from_kernel_nofault(), so bracket
+ * the call with user_access_save()/restore(): that clears AC for the whole
+ * record write (the kasan_report() pattern) and keeps SMAP/PAN protection in
+ * force while the collector runs. It compiles to nothing on architectures
+ * without the feature.
+ */
+#ifdef CONFIG_KCOV_DATAFLOW_ARGS
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,
+			   u64 *offsets, u32 num_fields);
+
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,
+			   u64 *offsets, u32 num_fields)
+{
+	unsigned long ua_flags = user_access_save();
+
+	kcov_df_write(KCOV_DF_TYPE_ENTRY, pc, arg_idx, arg_size, arg_ptr,
+		      offsets, num_fields);
+	user_access_restore(ua_flags);
+}
+EXPORT_SYMBOL(__sanitizer_cov_trace_args);
+#endif
+
+#ifdef CONFIG_KCOV_DATAFLOW_RET
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,
+			  u64 *offsets, u32 num_fields);
+
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,
+			  u64 *offsets, u32 num_fields)
+{
+	unsigned long ua_flags = user_access_save();
+
+	kcov_df_write(KCOV_DF_TYPE_RET, pc, 0, ret_size, ret_val,
+		      offsets, num_fields);
+	user_access_restore(ua_flags);
+}
+EXPORT_SYMBOL(__sanitizer_cov_trace_ret);
+#endif
+
+#ifdef KCOV_DF_HAVE_CMP
+/*
+ * Comparison capture (input-to-state). Reached from the shared
+ * __sanitizer_cov_trace_cmp*() callbacks (kcov.c) via kcov_trace_cmp()
+ * (linux/kcov.h), which fans out to mainline kcov and, when this task has a
+ * dataflow session, here as well, so trace-cmp operand pairs land in the SAME
+ * unified TLV buffer as the arg/ret records. Both operands are recorded, so a
+ * userspace consumer can use them for input-to-state matching, complementing
+ * the arg/ret records.
+ *
+ * Record: [header(CMP|nvals=2|seq)][pc][cmp_type][arg1][arg2].
+ * cmp_type carries KCOV_CMP_SIZE()/KCOV_CMP_CONST bits (see linux/kcov.h) so the
+ * consumer knows operand width and whether one side was a compile-time constant.
+ *
+ * On objtool's uaccess_safe_builtin[] list, so this function makes no
+ * out-of-line call (kcov_df_reserve() and the helpers are __always_inline).
+ */
+noinline notrace __no_sanitize_coverage void
+kcov_df_trace_cmp(u64 cmp_type, u64 arg1, u64 arg2, u64 ip)
+{
+	struct task_struct *t = current;
+	u64 *area;
+	unsigned long start_index;
+	u32 seq;
+
+	if (kcov_df_inert_context())
+		return;
+	if (!t->kcov_df_enabled)
+		return;
+	/* Same recursion guard as kcov_df_write(): bit 31 of the seq counter. */
+	if (t->kcov_df_seq & KCOV_DF_SEQ_GUARD)
+		return;
+	t->kcov_df_seq |= KCOV_DF_SEQ_GUARD;
+	barrier();
+
+	area = (u64 *)t->kcov_df_area;
+	if (!area)
+		goto out;
+
+	/* Single-writer exact-count reservation: see kcov_df_reserve(). */
+	if (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(2), &start_index))
+		goto out;
+
+	seq = kcov_df_next_seq(t);
+	area[start_index]     = kcov_df_hdr(KCOV_DF_TYPE_CMP, 2, 0, 0, seq);
+	area[start_index + 1] = kcov_df_canonicalize_ip(ip);
+	area[start_index + 2] = cmp_type;
+	area[start_index + 3] = arg1;
+	area[start_index + 4] = arg2;
+out:
+	barrier();
+	t->kcov_df_seq &= ~KCOV_DF_SEQ_GUARD;
+}
+EXPORT_SYMBOL(kcov_df_trace_cmp);
+#endif /* KCOV_DF_HAVE_CMP */
+
+/* Called from kernel/fork.c to clear inherited state. */
+void kcov_dataflow_task_init(struct task_struct *t)
+{
+	t->kcov_df_area = NULL;
+	t->kcov_df_size = 0;
+	t->kcov_df_seq = 0;
+	t->kcov_df_enabled = false;
+	t->kcov_df = NULL;
+	t->kcov_df_remote_depth = 0;
+}
+
+/* Called from kernel/exit.c to tear down the exiting task's session, if any. */
+void kcov_dataflow_task_exit(struct task_struct *t)
+{
+	struct kcov_dataflow *df = t->kcov_df;
+
+	if (!df)
+		return;
+
+	if (t->kcov_df_remote_depth > 0) {
+		/*
+		 * A remote kworker exited between kcov_df_remote_start() and
+		 * _stop() (should not happen -- they bracket a single work item).
+		 * Defensive: drop its partial scratch and release the ref so
+		 * neither the buffer nor the object leaks.
+		 */
+		void *scratch = t->kcov_df_area;
+
+		t->kcov_df_enabled = false;
+		t->kcov_df_area = NULL;
+		t->kcov_df_size = 0;
+		t->kcov_df = NULL;
+		t->kcov_df_remote_depth = 0;
+		vfree(scratch);
+		kcov_df_put(df);
+		return;
+	}
+
+	/*
+	 * Local (KCOV_DF_ENABLE) session on the exiting task. Mirror
+	 * kcov_task_exit(): unwire the task, clear df->t so the object never
+	 * keeps a pointer to a freed task_struct (which a later ioctl or
+	 * close() would compare against current), release the cmp key this
+	 * session held and drop the session's reference.
+	 */
+	t->kcov_df_enabled = false;
+	t->kcov_df_area = NULL;
+	t->kcov_df_size = 0;
+	t->kcov_df = NULL;
+
+	mutex_lock(&df->lock);
+	WARN_ON_ONCE(df->t != t);
+	df->t = NULL;
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);
+	mutex_unlock(&df->lock);
+	kcov_df_put(df);
+}
+
+/* File operations for /sys/kernel/debug/kcov_dataflow */
+
+static int kcov_df_open(struct inode *inode, struct file *filep)
+{
+	struct kcov_dataflow *df;
+
+	df = kzalloc_obj(struct kcov_dataflow, GFP_KERNEL);
+	if (!df)
+		return -ENOMEM;
+	mutex_init(&df->lock);
+	refcount_set(&df->refcount, 1);	/* the open fd's reference */
+	filep->private_data = df;
+	return nonseekable_open(inode, filep);
+}
+
+/*
+ * Unwire the local session that @current holds on @df. Caller holds df->lock
+ * and must drop the session's reference with kcov_df_put() after unlocking.
+ */
+static void kcov_df_disable_local(struct kcov_dataflow *df)
+{
+	lockdep_assert_held(&df->lock);
+	WARN_ON_ONCE(df->t != current || current->kcov_df != df);
+
+	current->kcov_df_enabled = false;
+	current->kcov_df_area = NULL;
+	current->kcov_df_size = 0;
+	current->kcov_df = NULL;
+	df->t = NULL;
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);
+}
+
+static int kcov_df_close(struct inode *inode, struct file *filep)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	bool put_session = false;
+
+	/* Unpublish from remote hash: no new users can start */
+	kcov_df_remote_unpublish(df);
+
+	mutex_lock(&df->lock);
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);
+	/*
+	 * Only the enabled task can unwire its own session. If another task
+	 * (a sibling thread, a fork()ed child, an SCM_RIGHTS recipient) does
+	 * the final close(), the enabled task keeps its reference and keeps
+	 * collecting until it exits, exactly like mainline kcov.
+	 */
+	if (df->t == current) {
+		kcov_df_disable_local(df);
+		put_session = true;
+	}
+	mutex_unlock(&df->lock);
+
+	if (put_session)
+		kcov_df_put(df);
+	/*
+	 * Drop the fd's reference. If remote workers or the enabled task still
+	 * hold refs, the LAST of them frees ->area via kcov_df_put() -- no drain
+	 * loop, no lost-decrement wedge. The hash entry was already unpublished
+	 * above, so no new remote user can start on this object.
+	 */
+	kcov_df_put(df);
+	return 0;
+}
+
+static int kcov_df_mmap(struct file *filep, struct vm_area_struct *vma)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	unsigned long size, off;
+	struct page *page;
+	void *area;
+	int res = 0;
+
+	mutex_lock(&df->lock);
+	size = df->size * sizeof(u64);
+	if (!df->area || vma->vm_pgoff != 0 ||
+	    vma->vm_end - vma->vm_start != size) {
+		res = -EINVAL;
+		goto out;
+	}
+	area = df->area;
+	mutex_unlock(&df->lock);
+
+	vm_flags_set(vma, VM_DONTEXPAND);
+	for (off = 0; off < size; off += PAGE_SIZE) {
+		page = vmalloc_to_page(area + off);
+		res = vm_insert_page(vma, vma->vm_start + off, page);
+		if (res)
+			return res;
+	}
+	return 0;
+out:
+	mutex_unlock(&df->lock);
+	return res;
+}
+
+static long kcov_df_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	bool put_session = false;
+	unsigned long size;
+	u64 handle = 0;
+	int res = 0;
+
+	/*
+	 * Fetch the remote handle from user space before taking df->lock.
+	 * get_user() may fault and take mmap_lock, but kcov_df_mmap() takes
+	 * df->lock while holding mmap_lock -- doing the copy under df->lock
+	 * would invert that order and deadlock (reported by lockdep).
+	 */
+	if (cmd == KCOV_DF_REMOTE_ENABLE && get_user(handle, (u64 __user *)arg))
+		return -EFAULT;
+
+	mutex_lock(&df->lock);
+	switch (cmd) {
+	case KCOV_DF_INIT_TRACK:
+		if (df->area) {
+			res = -EBUSY;
+			break;
+		}
+		size = arg;
+		if (size < 2 || size > (128 << 20) / sizeof(u64)) {
+			res = -EINVAL;
+			break;
+		}
+		mutex_unlock(&df->lock);
+		{
+			void *area = vmalloc_user(size * sizeof(u64));
+
+			if (!area)
+				return -ENOMEM;
+			mutex_lock(&df->lock);
+			if (df->area) {
+				mutex_unlock(&df->lock);
+				vfree(area);
+				return -EBUSY;
+			}
+			df->area = area;
+			df->size = size;
+		}
+		break;
+
+	case KCOV_DF_ENABLE:
+		/*
+		 * One writer per buffer: refuse if this object already has a
+		 * local session, if this task already has one (on any fd), or
+		 * if the buffer is (or may still be) a remote merge target -- a
+		 * published handle, or workers still in flight after
+		 * KCOV_DF_REMOTE_DISABLE (any ref beyond the fd's own). The
+		 * local reservation is a plain read-modify-write of area[0]
+		 * that must never race kcov_df_merge()'s atomic one.
+		 */
+		if (!df->area || df->t || df->remote_handle ||
+		    refcount_read(&df->refcount) != 1 || current->kcov_df) {
+			res = -EBUSY;
+			break;
+		}
+		kcov_df_fault_in_area(df->area, df->size);
+		kcov_df_get(df);	/* put in KCOV_DF_DISABLE, close() or task exit */
+		df->t = current;
+		current->kcov_df = df;
+		current->kcov_df_area = df->area;
+		current->kcov_df_size = df->size;
+		current->kcov_df_seq = 0;
+		current->kcov_df_remote_depth = 0;
+		/* Publish the session state before the enable flag. */
+		barrier();
+		current->kcov_df_enabled = true;
+		kcov_df_cmp_key_hold(df, KCOV_DF_CMP_LOCAL);
+		break;
+
+	case KCOV_DF_DISABLE:
+		if (df->t != current) {
+			res = -EINVAL;
+			break;
+		}
+		kcov_df_disable_local(df);
+		put_session = true;
+		break;
+
+	case KCOV_DF_REMOTE_ENABLE: {
+		struct kcov_df_remote *remote;
+
+		if (!df->area ||
+		    !kcov_check_handle(handle, true, true, false)) {
+			res = -EINVAL;
+			break;
+		}
+		/*
+		 * One handle per fd (a second one would leak the first entry
+		 * and leave it pointing at a freed object after close()), and
+		 * never while a local session writes the buffer directly.
+		 */
+		if (df->t || df->remote_handle) {
+			res = -EBUSY;
+			break;
+		}
+		remote = kzalloc_obj(struct kcov_df_remote, GFP_KERNEL);
+		if (!remote) {
+			res = -ENOMEM;
+			break;
+		}
+		remote->handle = handle;
+		remote->df = df;
+		mutex_lock(&kcov_df_remote_lock);
+		if (kcov_df_remote_find(handle)) {
+			mutex_unlock(&kcov_df_remote_lock);
+			kfree(remote);
+			res = -EEXIST;
+			break;
+		}
+		hash_add(kcov_df_remote_map, &remote->hnode, handle);
+		df->remote_handle = handle;
+		mutex_unlock(&kcov_df_remote_lock);
+		kcov_df_cmp_key_hold(df, KCOV_DF_CMP_REMOTE);
+		break;
+	}
+
+	case KCOV_DF_REMOTE_DISABLE:
+		kcov_df_remote_unpublish(df);
+		kcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);
+		break;
+
+	default:
+		res = -ENOTTY;
+	}
+	mutex_unlock(&df->lock);
+
+	if (put_session)
+		kcov_df_put(df);
+	return res;
+}
+
+/* Remote dataflow implementation */
+
+/*
+ * Open a remote dataflow section on this task for @handle. Must be called from
+ * sleepable task context (it takes a mutex and may vmalloc() the scratch); in
+ * softirq/hardirq context it is a no-op, as is the matching stop, so the pair
+ * stays balanced for a call site that brackets a softirq-reachable region.
+ */
+void kcov_df_remote_start(u64 handle)
+{
+	struct kcov_df_remote *remote;
+	struct kcov_dataflow *df;
+	void *scratch;
+
+	/* Dataflow remote coverage is collected in task (kworker) context only. */
+	if (!in_task())
+		return;
+	/*
+	 * A task should only run one session at a time (KCOV's rule). If a
+	 * buggy caller nests inside a remote section, don't re-init and don't
+	 * take a second ref -- just count the depth so the matching inner
+	 * stop() leaves the outer session intact (see kcov_df_remote_stop()).
+	 * Coverage from the nested region is attributed to the outer handle,
+	 * which is safe (no corruption, no early free) even though it is
+	 * imprecise. Inside a local (KCOV_DF_ENABLE) session the depth stays
+	 * 0, so the inner stop() is a no-op and the local session's wiring is
+	 * left untouched; its records simply go to its own buffer.
+	 *
+	 * This check comes first so that every early return below only ever
+	 * happens with no session live -- then the matching stop() has nothing
+	 * to tear down and can never truncate an outer section.
+	 */
+	if (current->kcov_df) {
+		WARN_ON_ONCE(1);
+		if (current->kcov_df_remote_depth > 0 &&
+		    current->kcov_df_remote_depth < INT_MAX)
+			current->kcov_df_remote_depth++;
+		return;
+	}
+	if (!handle)
+		return;
+
+	/* mutex_lock()'s might_sleep() reports an atomic (non-sleepable) caller. */
+	mutex_lock(&kcov_df_remote_lock);
+	remote = kcov_df_remote_find(handle);
+	if (!remote || !remote->df || !remote->df->area) {
+		mutex_unlock(&kcov_df_remote_lock);
+		return;
+	}
+	df = remote->df;
+	kcov_df_get(df);		/* keep @df (and ->area) alive until _stop() */
+	scratch = kcov_df_scratch_get();	/* reuse a pooled scratch if any */
+	mutex_unlock(&kcov_df_remote_lock);
+
+	if (!scratch) {
+		scratch = vmalloc(KCOV_DF_REMOTE_WORDS * sizeof(u64));
+		if (!scratch) {
+			kcov_df_put(df);
+			return;
+		}
+	}
+	((u64 *)scratch)[0] = 0;	/* reset the scratch write cursor */
+	kcov_df_fault_in_area(scratch, KCOV_DF_REMOTE_WORDS);
+
+	/*
+	 * Point this task at its OWN private scratch, NOT df->area. It collects
+	 * here while it runs; kcov_df_remote_stop() merges it into the shared
+	 * buffer. So multiple kworkers on one handle never write the same buffer.
+	 */
+	current->kcov_df_area = scratch;
+	current->kcov_df_size = KCOV_DF_REMOTE_WORDS;
+	current->kcov_df_seq = 0;
+	current->kcov_df = df;		/* pocket it for _stop(); no hash relookup */
+	current->kcov_df_remote_depth = 1;
+	/*
+	 * Publish all session state BEFORE the enable flag (mirrors kcov_start()).
+	 * kcov_df_write() gates on kcov_df_enabled and then reads kcov_df_area, so
+	 * the buffer/handle must be visible first; the barrier keeps the compiler
+	 * from hoisting the enable above them.
+	 */
+	barrier();
+	current->kcov_df_enabled = true;
+}
+EXPORT_SYMBOL_GPL(kcov_df_remote_start);
+
+void kcov_df_remote_stop(void)
+{
+	struct kcov_dataflow *df = current->kcov_df;
+	void *scratch;
+
+	/*
+	 * Same context rule as kcov_df_remote_start(): a stop() in softirq
+	 * context pairs with a start() that was a no-op, and must not touch
+	 * the interrupted task's live session.
+	 */
+	if (!in_task())
+		return;
+	/* No remote session (a local session ignores a stray stop). */
+	if (!df || current->kcov_df_remote_depth == 0)
+		return;
+
+	/*
+	 * Unwind a nested start() (buggy caller): only the OUTERMOST stop tears
+	 * the session down. Inner stops just decrement the depth and return, so
+	 * the buffer/ref survive until the worker is really done with them.
+	 */
+	if (--current->kcov_df_remote_depth > 0)
+		return;
+
+	scratch = current->kcov_df_area;
+
+	/*
+	 * Stop writing FIRST: clear the per-task pointers so this task can no
+	 * longer enter kcov_df_write() / touch the scratch. Then it is safe to
+	 * merge and recycle the scratch and drop the ref.
+	 */
+	current->kcov_df_enabled = false;
+	current->kcov_df_area = NULL;
+	current->kcov_df_size = 0;
+	current->kcov_df = NULL;
+
+	if (scratch) {
+		/*
+		 * Publish this worker's records into the shared buffer,
+		 * then return the scratch to the pool for the next worker.
+		 */
+		kcov_df_merge(df, scratch);
+		mutex_lock(&kcov_df_remote_lock);
+		kcov_df_scratch_put(scratch);
+		mutex_unlock(&kcov_df_remote_lock);
+	}
+
+	/*
+	 * Drop the ref taken in kcov_df_remote_start(). If this is the last one,
+	 * kcov_df_put() frees ->area right here -- safe, because no task writes
+	 * ->area directly anymore (workers write scratch; the merge above is
+	 * done). Dropping via the pocketed @df (not a hash lookup) means an
+	 * already-unpublished entry can never strand the count.
+	 */
+	kcov_df_put(df);
+}
+EXPORT_SYMBOL_GPL(kcov_df_remote_stop);
+
+static const struct file_operations kcov_df_fops = {
+	.open		= kcov_df_open,
+	.unlocked_ioctl	= kcov_df_ioctl,
+	.compat_ioctl	= kcov_df_ioctl,
+	.mmap		= kcov_df_mmap,
+	.release	= kcov_df_close,
+};
+
+/*
+ * Reclaim idle per-worker scratch under memory pressure. The pool otherwise only
+ * ever grows to the peak number of concurrent remote kworkers (each area is 8 MiB)
+ * and is never returned to the allocator; a shrinker lets the VM take the idle
+ * (parked) areas back when it needs the memory. Only pooled areas are freeable;
+ * in-use scratch is not on the list. mutex_trylock keeps the shrinker best-effort
+ * and free of any lock-ordering risk.
+ */
+static unsigned long
+kcov_df_scratch_shrink_count(struct shrinker *sh, struct shrink_control *sc)
+{
+	unsigned long nr;
+
+	if (!mutex_trylock(&kcov_df_remote_lock))
+		return 0;
+	nr = kcov_df_scratch_pool_nr;
+	mutex_unlock(&kcov_df_remote_lock);
+	return nr ? nr : SHRINK_EMPTY;
+}
+
+static unsigned long
+kcov_df_scratch_shrink_scan(struct shrinker *sh, struct shrink_control *sc)
+{
+	struct kcov_df_scratch *s, *tmp;
+	LIST_HEAD(victims);
+	unsigned long freed = 0;
+
+	if (!mutex_trylock(&kcov_df_remote_lock))
+		return SHRINK_STOP;
+	/*
+	 * Detach victims under the lock; free them (each 8 MiB) after unlocking
+	 * so the vfree() latency stays off concurrent remote_start()/stop().
+	 */
+	while (freed < sc->nr_to_scan && !list_empty(&kcov_df_scratch_pool)) {
+		s = list_first_entry(&kcov_df_scratch_pool,
+				     struct kcov_df_scratch, list);
+		list_move(&s->list, &victims);
+		kcov_df_scratch_pool_nr--;
+		freed++;
+	}
+	mutex_unlock(&kcov_df_remote_lock);
+
+	list_for_each_entry_safe(s, tmp, &victims, list)
+		vfree(s);
+	return freed;
+}
+
+static int __init kcov_dataflow_init(void)
+{
+	struct shrinker *shrinker;
+
+	debugfs_create_file_unsafe("kcov_dataflow", 0600, NULL, NULL,
+				   &kcov_df_fops);
+
+	shrinker = shrinker_alloc(0, "kcov-df-scratch");
+	if (shrinker) {
+		shrinker->count_objects = kcov_df_scratch_shrink_count;
+		shrinker->scan_objects = kcov_df_scratch_shrink_scan;
+		shrinker->seeks = DEFAULT_SEEKS;
+		shrinker_register(shrinker);
+	} else {
+		pr_warn("scratch shrinker unavailable, idle remote scratch areas will not be reclaimed\n");
+	}
+	return 0;
+}
+device_initcall(kcov_dataflow_init);
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625e..6b724ae713ce1 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2219,6 +2219,58 @@ config KCOV_SELFTEST
 	  On test failure, causes the kernel to panic. Recommended to be
 	  enabled, ensuring critical functionality works as intended.
 
+config KCOV_DATAFLOW_ARGS
+	bool "Enable KCOV dataflow: function argument capture"
+	depends on KCOV
+	depends on CC_IS_CLANG
+	depends on DEBUG_INFO
+	depends on $(cc-option,-fsanitize-coverage=trace-args)
+	depends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-args)
+	help
+	  Captures function arguments at entry via /sys/kernel/debug/kcov_dataflow.
+	  Struct pointer arguments are auto-expanded using compiler DebugInfo
+	  metadata, recording individual field values at runtime.
+	  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.
+	  Requires clang with -fsanitize-coverage=trace-args support (and,
+	  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus
+	  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under
+	  "Compile-time checks and compiler options" to satisfy DEBUG_INFO.
+
+config KCOV_DATAFLOW_RET
+	bool "Enable KCOV dataflow: return value capture"
+	depends on KCOV
+	depends on CC_IS_CLANG
+	depends on DEBUG_INFO
+	depends on $(cc-option,-fsanitize-coverage=trace-ret)
+	depends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-ret)
+	help
+	  Captures function return values via /sys/kernel/debug/kcov_dataflow.
+	  Struct pointer returns are auto-expanded using compiler DebugInfo
+	  metadata, recording individual field values at runtime.
+	  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.
+	  Requires clang with -fsanitize-coverage=trace-ret support (and,
+	  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus
+	  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under
+	  "Compile-time checks and compiler options" to satisfy DEBUG_INFO.
+
+config KCOV_DATAFLOW_NO_INLINE
+	bool "Disable inlining for dataflow-instrumented files"
+	depends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET
+	help
+	  Adds -fno-inline to files instrumented with KCOV_DATAFLOW.
+	  This ensures every function boundary is preserved, giving
+	  complete argument visibility. Disable for lower overhead at the
+	  cost of losing argument records for inlined functions.
+
+config KCOV_DATAFLOW_INSTRUMENT_ALL
+	bool "Instrument all kernel code with dataflow coverage"
+	depends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET
+	help
+	  Instrument all kernel objects with trace-args/trace-ret
+	  automatically. Individual files or directories can opt out
+	  with KCOV_DATAFLOW_file.o := n or KCOV_DATAFLOW := n.
+	  Warning: significantly increases code size and boot time.
+
 menuconfig RUNTIME_TESTING_MENU
 	bool "Runtime Testing"
 	default y
diff --git a/scripts/Makefile.kcov b/scripts/Makefile.kcov
index 78305a84ba9d2..5fd2aa69d8fd5 100644
--- a/scripts/Makefile.kcov
+++ b/scripts/Makefile.kcov
@@ -9,3 +9,20 @@ kcov-rflags-$(CONFIG_KCOV_ENABLE_COMPARISONS)	+= -Cllvm-args=-sanitizer-coverage
 
 export CFLAGS_KCOV := $(kcov-flags-y)
 export RUSTFLAGS_KCOV := $(kcov-rflags-y)
+
+# KCOV dataflow: trace function args and return values. Each kind is gated by
+# its own Kconfig symbol, matching the #ifdef around the callback it emits calls
+# to in kernel/kcov_dataflow.c (an instrumented object must never reference a
+# callback that is not compiled in). Both variables are empty on a KCOV-only
+# kernel, so a stray per-file KCOV_DATAFLOW_file.o := y is harmless there.
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -fsanitize-coverage=trace-args
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_RET) += -fsanitize-coverage=trace-ret
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_NO_INLINE) += -fno-inline
+
+# Rust: only add the trace-args/ret llvm-args (sancov-module pass and level=3
+# are already provided by RUSTFLAGS_KCOV since KCOV_DATAFLOW depends on KCOV).
+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -Cllvm-args=-sanitizer-coverage-trace-args
+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_RET) += -Cllvm-args=-sanitizer-coverage-trace-ret
+
+export CFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-flags-y)
+export RUSTFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-rflags-y)
diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
index 0a4fdd8bd975d..b32fa67ce99af 100644
--- a/scripts/Makefile.lib
+++ b/scripts/Makefile.lib
@@ -88,6 +88,20 @@ _c_flags += $(if $(patsubst n%,, \
 _rust_flags += $(if $(patsubst n%,, \
 	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)$(if $(is-kernel-object),$(CONFIG_KCOV_INSTRUMENT_ALL))), \
 	$(RUSTFLAGS_KCOV))
+# KCOV dataflow. The outer test only honours an explicit KCOV opt-out
+# (KCOV_INSTRUMENT_file.o := n / KCOV_INSTRUMENT := n, the noinstr exclusions):
+# it does not require a KCOV opt-in, so per-file KCOV_DATAFLOW_file.o := y works
+# for modules and out-of-tree objects too. The inner test is the dataflow opt-in:
+# per-file/per-directory, or every kernel object under
+# CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL.
+_c_flags += $(if $(patsubst n%,, \
+	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \
+	$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \
+	$(CFLAGS_KCOV_DATAFLOW)))
+_rust_flags += $(if $(patsubst n%,, \
+	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \
+	$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \
+	$(RUSTFLAGS_KCOV_DATAFLOW)))
 endif
 
 #
diff --git a/tools/objtool/check.c b/tools/objtool/check.c
index 464f6c9d9ff0b..ae6fe47886395 100644
--- a/tools/objtool/check.c
+++ b/tools/objtool/check.c
@@ -1219,6 +1219,10 @@ static const char *uaccess_safe_builtin[] = {
 	"__tsan_unaligned_write16",
 	/* KCOV */
 	"write_comp_data",
+	/* KCOV dataflow */
+	"kcov_df_trace_cmp",
+	"__sanitizer_cov_trace_args",
+	"__sanitizer_cov_trace_ret",
 	"check_kcov_mode",
 	"__sanitizer_cov_trace_pc",
 	"__sanitizer_cov_trace_const_cmp1",
diff --git a/tools/testing/selftests/kcov_dataflow/.gitignore b/tools/testing/selftests/kcov_dataflow/.gitignore
new file mode 100644
index 0000000000000..4f2957a017957
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+user_ioctl/user_ioctl
+binderfs/binderfs_test
+__pycache__/
diff --git a/tools/testing/selftests/kcov_dataflow/Kbuild b/tools/testing/selftests/kcov_dataflow/Kbuild
new file mode 100644
index 0000000000000..2e19e9008fdca
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Kbuild
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test modules, built as external modules against the configured kernel tree
+# by the selftest Makefile ("make -C $(KDIR) M=$(CURDIR) modules"). Every
+# directory opts its object into dataflow instrumentation with
+# KCOV_DATAFLOW_<object>.o := y, the same per-file switch in-tree code uses.
+obj-m				+= rust_ffi_contract/
+obj-m				+= eight_struct_args_c/
+obj-$(CONFIG_RUST)		+= eight_struct_args_rust/
+obj-$(CONFIG_RUST)		+= rust_kworker_remote/
diff --git a/tools/testing/selftests/kcov_dataflow/Makefile b/tools/testing/selftests/kcov_dataflow/Makefile
new file mode 100644
index 0000000000000..fc979e2d4ecc3
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Makefile
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# kcov_dataflow selftests
+#
+# user_ioctl and binderfs are ordinary kselftest programs. The test modules
+# (one per directory, listed in Kbuild) are built by kbuild against KDIR and
+# are loaded, triggered and checked by test_modules.py; trigger-view.py is
+# the interactive viewer the runner is built on.
+#
+# KDIR is the configured kernel build tree. It defaults to the source tree
+# this directory lives in; point it at the O= directory for out-of-tree
+# builds. Pass the same LLVM=1 CC=clang [RUSTC= RUST_LIB_SRC=] the kernel was
+# built with so that kbuild picks the toolchain that has the trace-args and
+# trace-ret passes.
+KDIR ?= $(abspath ../../../..)
+
+TEST_GEN_PROGS := user_ioctl/user_ioctl binderfs/binderfs_test
+TEST_PROGS := test_modules.py
+TEST_FILES := trigger-view.py
+
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+
+# The .ko files kbuild produces for KDIR's configuration, so that they are
+# built by "all" and copied by "install"; the Rust modules need CONFIG_RUST.
+KMODS := rust_ffi_contract eight_struct_args_c
+ifneq ($(shell grep -s ^CONFIG_RUST=y $(KDIR)/.config),)
+KMODS += eight_struct_args_rust rust_kworker_remote
+endif
+TEST_GEN_FILES := $(foreach m,$(KMODS),$(m)/$(m).ko)
+
+include ../lib.mk
+
+ifneq ($(wildcard $(KDIR)/.config),)
+$(TEST_GEN_FILES): modules
+modules:
+	$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) modules
+clean_modules:
+	$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) clean
+else
+$(TEST_GEN_FILES):
+	@echo "SKIP $(notdir $@): no configured kernel tree at $(KDIR), set KDIR="
+clean_modules:
+endif
+
+clean: clean_modules
+.PHONY: modules clean_modules
diff --git a/tools/testing/selftests/kcov_dataflow/README.rst b/tools/testing/selftests/kcov_dataflow/README.rst
new file mode 100644
index 0000000000000..1929a357aca47
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/README.rst
@@ -0,0 +1,69 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests
+=======================
+
+Selftests for ``/sys/kernel/debug/kcov_dataflow`` (see
+Documentation/dev-tools/kcov-dataflow.rst).
+
+Layout
+------
+
+Makefile, Kbuild
+    kselftest build: the C programs are built by lib.mk, the test modules
+    (one directory each, listed in Kbuild) by kbuild against ``KDIR``.
+user_ioctl/
+    ioctl interface test (kselftest harness, TAP).
+binderfs/
+    binder ioctls under recording (TAP).
+rust_ffi_contract/, eight_struct_args_c/, eight_struct_args_rust/,
+rust_kworker_remote/
+    test modules; each README.rst says what the module exercises.
+test_modules.py
+    KTAP runner: loads every module, triggers it with recording active and
+    checks the captured arguments, struct fields and return values against
+    the values the module uses. Modules that are not built are SKIPped.
+trigger-view.py
+    Interactive viewer the runner is built on (call tree or ``--raw``
+    records, kallsyms/addr2line symbolization, ``--remote`` capture).
+
+Kernel
+------
+
+The kernel and the modules must be built with a clang that has the
+trace-args/trace-ret passes (and, for the Rust modules, a rustc built
+against that LLVM). The config fragment ``config`` lists what the tests
+need; with virtme-ng::
+
+    vng --build --config tools/testing/selftests/kcov_dataflow/config \
+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+Build
+-----
+
+From the kernel tree, with the same toolchain variables::
+
+    make LLVM=1 headers
+    make -C tools/testing/selftests TARGETS=kcov_dataflow \
+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+``KDIR`` defaults to the source tree; pass ``KDIR=<O dir>`` for out-of-tree
+builds. The Rust modules are built only when ``KDIR/.config`` has
+``CONFIG_RUST=y``. ``make ... install INSTALL_PATH=<dir>`` produces a
+self-contained tree with ``run_kselftest.sh``.
+
+Run
+---
+
+On the target (root, debugfs mounted)::
+
+    vng --user root --exec \
+        "tools/testing/selftests/kcov_dataflow/test_modules.py"
+    tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl
+    tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
+
+or, from an installed tree, ``run_kselftest.sh -c kcov_dataflow``.
+``test_modules.py -t <module> -C 8`` runs one module and echoes eight
+records of context around each module record; ``trigger-view.py <module>
+[--raw] [-C N] [--remote] [--vmlinux vmlinux]`` shows the capture
+without checking it.
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/Makefile b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile
new file mode 100644
index 0000000000000..b35de62649924
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+# Standalone build of the binderfs test: make -C tools/testing/selftests/kcov_dataflow/binderfs
+TEST_GEN_PROGS := binderfs_test
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+include ../../lib.mk
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/README.rst b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst
new file mode 100644
index 0000000000000..7fcdce1955c19
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: binderfs
+=================================
+
+Exercises the binder driver via binderfs with kcov_dataflow recording
+active and verifies that argument records are captured at the binder
+ioctl boundaries. Needs CONFIG_ANDROID_BINDERFS=y and binder instrumented
+(``KCOV_DATAFLOW := y`` in drivers/android/Makefile or
+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y); SKIPs without binderfs::
+
+  make -C tools/testing/selftests TARGETS=kcov_dataflow
+  tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c
new file mode 100644
index 0000000000000..650798e09b20a
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binderfs selftest for kcov_dataflow
+ *
+ * Exercises the binder driver via binderfs with kcov_dataflow recording
+ * active, then verifies that function argument records were captured at
+ * binder ioctl boundaries.
+ *
+ * Requires: CONFIG_ANDROID_BINDER_IPC=y (or _RUST), CONFIG_ANDROID_BINDERFS=y
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <linux/android/binder.h>
+#include <linux/android/binderfs.h>
+#include <linux/kcov_dataflow.h>
+
+
+#define BUF_SIZE	(1 << 20)
+#define BINDERFS_PATH	"/tmp/binderfs_test"
+#define BINDER_DEV	BINDERFS_PATH "/my_binder"
+
+static int setup_binderfs(void)
+{
+	struct binderfs_device dev = {};
+
+	mkdir(BINDERFS_PATH, 0755);
+
+	if (mount("binder", BINDERFS_PATH, "binder", 0, NULL)) {
+		if (errno == ENODEV || errno == ENOENT) {
+			printf("SKIP: binderfs not available\n");
+			return -1;
+		}
+		perror("mount binderfs");
+		return -1;
+	}
+
+	/* Create a binder device via BINDER_CTL_ADD ioctl */
+	int ctl_fd;
+
+	ctl_fd = open(BINDERFS_PATH "/binder-control", O_RDONLY);
+	if (ctl_fd < 0) {
+		perror("open binder-control");
+		umount(BINDERFS_PATH);
+		return -1;
+	}
+
+	strcpy(dev.name, "my_binder");
+	if (ioctl(ctl_fd, BINDER_CTL_ADD, &dev) && errno != EEXIST) {
+		perror("BINDER_CTL_ADD");
+		close(ctl_fd);
+		umount(BINDERFS_PATH);
+		return -1;
+	}
+	close(ctl_fd);
+	return 0;
+}
+
+static void cleanup_binderfs(void)
+{
+	umount(BINDERFS_PATH);
+	rmdir(BINDERFS_PATH);
+}
+
+int main(void)
+{
+	uint64_t *buf;
+	int df_fd, binder_fd;
+	uint64_t total;
+	int valid = 0;
+
+	printf("TAP version 13\n");
+	printf("1..3\n");
+
+	/* Setup binderfs */
+	if (setup_binderfs()) {
+		printf("ok 1 # SKIP binderfs not available\n");
+		printf("ok 2 # SKIP\n");
+		printf("ok 3 # SKIP\n");
+		return 0;
+	}
+
+	/* Open kcov_dataflow */
+	df_fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+	if (df_fd < 0) {
+		printf("not ok 1 cannot open kcov_dataflow\n");
+		cleanup_binderfs();
+		return 1;
+	}
+
+	if (ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)) {
+		printf("not ok 1 INIT_TRACK failed\n");
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+		   PROT_READ | PROT_WRITE, MAP_SHARED, df_fd, 0);
+	if (buf == MAP_FAILED) {
+		printf("not ok 1 mmap failed\n");
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	printf("ok 1 kcov_dataflow.binderfs_setup\n");
+
+	/* Open binder device */
+	binder_fd = open(BINDER_DEV, O_RDWR | O_CLOEXEC);
+	if (binder_fd < 0) {
+		printf("not ok 2 cannot open %s: %s\n", BINDER_DEV,
+		       strerror(errno));
+		munmap(buf, BUF_SIZE * sizeof(uint64_t));
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	/* Enable recording and exercise binder ioctls */
+	ioctl(df_fd, KCOV_DF_ENABLE, 0);
+	__atomic_store_n(&buf[0], 0, __ATOMIC_RELAXED);
+
+	/* BINDER_VERSION - simple ioctl that exercises the binder path */
+	struct binder_version ver = {};
+
+	ioctl(binder_fd, BINDER_VERSION, &ver);
+
+	/* BINDER_SET_MAX_THREADS */
+	uint32_t max_threads = 4;
+
+	ioctl(binder_fd, BINDER_SET_MAX_THREADS, &max_threads);
+
+	ioctl(df_fd, KCOV_DF_DISABLE, 0);
+
+	total = __atomic_load_n(&buf[0], __ATOMIC_RELAXED);
+	close(binder_fd);
+
+	if (total > 0)
+		printf("ok 2 kcov_dataflow.binderfs_captured # %lu words\n",
+		       (unsigned long)total);
+	else
+		printf("not ok 2 kcov_dataflow.binderfs_captured # 0 words\n");
+
+	/*
+	 * Walk the records: every header must carry a known type and at least
+	 * one value word, the walk must end exactly at area[0], and at least one
+	 * ENTRY/RET record must come from the binder ioctls (CMP records are
+	 * interleaved with CONFIG_KCOV_ENABLE_COMPARISONS=y).
+	 */
+	if (total <= BUF_SIZE - 1) {
+		uint64_t pos = 1, end = 1 + total;
+		unsigned long nargs = 0;
+
+		while (pos + KCOV_DF_RECORD_HDR_WORDS <= end) {
+			uint64_t hdr = buf[pos];
+			uint32_t type = KCOV_DF_HDR_TYPE(hdr);
+			uint32_t nvals = KCOV_DF_HDR_NVALS(hdr);
+
+			if (nvals < 1 || (type != KCOV_DF_TYPE_ENTRY &&
+					  type != KCOV_DF_TYPE_RET &&
+					  type != KCOV_DF_TYPE_CMP))
+				break;
+			if (type != KCOV_DF_TYPE_CMP)
+				nargs++;
+			pos += KCOV_DF_RECORD_WORDS(nvals);
+		}
+		if (pos == end && nargs > 0)
+			valid = 1;
+		else
+			printf("# walk stopped at word %lu of %lu, %lu ENTRY/RET records\n",
+			       (unsigned long)pos, (unsigned long)end, nargs);
+	}
+
+	if (valid)
+		printf("ok 3 kcov_dataflow.binderfs_valid_records\n");
+	else
+		printf("not ok 3 kcov_dataflow.binderfs_valid_records\n");
+
+	printf("# Totals: pass:%d fail:%d skip:0\n",
+	       valid ? 3 : 2, valid ? 0 : 1);
+
+	munmap(buf, BUF_SIZE * sizeof(uint64_t));
+	close(df_fd);
+	cleanup_binderfs();
+	return valid ? 0 : 1;
+}
diff --git a/tools/testing/selftests/kcov_dataflow/config b/tools/testing/selftests/kcov_dataflow/config
new file mode 100644
index 0000000000000..7f3a2fda0641d
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/config
@@ -0,0 +1,11 @@
+CONFIG_KCOV=y
+CONFIG_KCOV_DATAFLOW_ARGS=y
+CONFIG_KCOV_DATAFLOW_RET=y
+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y
+CONFIG_KCOV_DATAFLOW_NO_INLINE=y
+CONFIG_DEBUG_INFO_DWARF5=y
+CONFIG_DEBUG_FS=y
+CONFIG_MODULES=y
+CONFIG_ANDROID_BINDER_IPC=y
+CONFIG_ANDROID_BINDERFS=y
+CONFIG_RUST=y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile
new file mode 100644
index 0000000000000..04ff83f0a9625
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := eight_struct_args_c.o
+KCOV_DATAFLOW_eight_struct_args_c.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst
new file mode 100644
index 0000000000000..62cddee78cd36
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: eight_struct_args_c
+============================================
+
+C module with 1-8 struct pointer arguments (flat s1..s8), value-nested
+st1..st8 and pointer-linked stp1..stp8 towers (on stack, kmalloc and
+vmalloc), pointer forwarding and a struct return value. Opted in with
+``KCOV_DATAFLOW_eight_struct_args_c.o := y``; test_modules.py checks the
+expanded fields (0x11, 0x22, ...) and every return value::
+
+  ./test_modules.py -t eight_struct_args_c
+  ./trigger-view.py eight_struct_args_c --raw
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c
new file mode 100644
index 0000000000000..c7d06a8e94c38
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c
@@ -0,0 +1,533 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * eight_struct_args_c.c - Verify kcov_dataflow captures struct pointer
+ * arguments with automatic field expansion.
+ *
+ * Three families of structs are exercised:
+ *
+ *  - Flat structs s1..s8: sN has N u64 members side by side; sf_N takes N
+ *    struct pointer args (s1*..sN*). Tests plain field expansion and multiple
+ *    struct-pointer arguments.
+ *
+ *  - Recursively (value) nested structs st1..st8: stN embeds every smaller
+ *    struct by value, so the nesting deepens with N:
+ *        st1 = { u64 field0 }
+ *        st2 = { u64 field0, st1 field1 }             // { v, {v} }
+ *        stN = { u64 field0, st1 field1, ... st(N-1) field(N-1) }
+ *    The deepest chain in st8 is eight levels deep. Used by the stack tests.
+ *
+ *  - Pointer-linked nested structs stp1..stp8: every member is a POINTER to a
+ *    separately allocated object, so the nesting is followed through the heap:
+ *        stp1 = { u64 *field0 }
+ *        stp2 = { u64 *field0, stp1 *field1 }         // { *v, *{v} }
+ *        stpN = { u64 *field0, stp1 *field1, ... stp(N-1) *field(N-1) }
+ *    Used by the dynamic-allocation (kmalloc/vmalloc) tests.
+ *
+ * Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct to invoke.
+ */
+#include <linux/module.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KCOV dataflow struct field expansion test (flat + nested)");
+
+/* Flat structs: sN has N u64 members. */
+struct s1 { u64 a; };
+struct s2 { u64 a; u64 b; };
+struct s3 { u64 a; u64 b; u64 c; };
+struct s4 { u64 a; u64 b; u64 c; u64 d; };
+struct s5 { u64 a; u64 b; u64 c; u64 d; u64 e; };
+struct s6 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; };
+struct s7 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; };
+struct s8 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; u64 h; };
+
+/*
+ * Recursively (value) nested structs: stN = { u64 field0; st1 field1; ...;
+ * st(N-1) field(N-1); }. Each stN contains every smaller struct by value, so
+ * the nesting depth grows with N (st8 is eight levels deep along its st7 chain).
+ */
+struct st1 { u64 field0; };
+struct st2 { u64 field0; struct st1 field1; };
+struct st3 { u64 field0; struct st1 field1; struct st2 field2; };
+struct st4 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+};
+struct st5 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+};
+struct st6 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+};
+struct st7 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+	struct st6 field6;
+};
+struct st8 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+	struct st6 field6;
+	struct st7 field7;
+};
+
+/*
+ * Pointer-linked nested structs: every member is a POINTER to a separately
+ * allocated object. stpN = { u64 *field0; stp1 *field1; ...; stp(N-1)
+ * *field(N-1); }. The dynamic-allocation tests build one of these per allocator.
+ */
+struct stp1 { u64 *field0; };
+struct stp2 { u64 *field0; struct stp1 *field1; };
+struct stp3 { u64 *field0; struct stp1 *field1; struct stp2 *field2; };
+struct stp4 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+};
+struct stp5 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+};
+struct stp6 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+};
+struct stp7 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+	struct stp6 *field6;
+};
+struct stp8 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+	struct stp6 *field6;
+	struct stp7 *field7;
+};
+
+/* Prototypes: sf_N takes N struct pointer arguments (s1*, s2*, ..., sN*) */
+u64 sf_1(struct s1 *a);
+u64 sf_2(struct s1 *a, struct s2 *b);
+u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c);
+u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e);
+u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f);
+u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f, struct s7 *g);
+u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f, struct s7 *g, struct s8 *h);
+
+/* stf_N takes a pointer to the value-nested stN and sums every reachable field0. */
+u64 stf_1(struct st1 *p);
+u64 stf_2(struct st2 *p);
+u64 stf_3(struct st3 *p);
+u64 stf_4(struct st4 *p);
+u64 stf_5(struct st5 *p);
+u64 stf_6(struct st6 *p);
+u64 stf_7(struct st7 *p);
+u64 stf_8(struct st8 *p);
+
+/* stpf_N follows the pointer-linked stpN and sums every reachable *field0. */
+u64 stpf_1(struct stp1 *p);
+u64 stpf_2(struct stp2 *p);
+u64 stpf_3(struct stp3 *p);
+u64 stpf_4(struct stp4 *p);
+u64 stpf_5(struct stp5 *p);
+u64 stpf_6(struct stp6 *p);
+u64 stpf_7(struct stp7 *p);
+u64 stpf_8(struct stp8 *p);
+
+noinline u64 sf_1(struct s1 *a) { return a->a; }
+EXPORT_SYMBOL(sf_1);
+
+noinline u64 sf_2(struct s1 *a, struct s2 *b) { return a->a + b->b; }
+EXPORT_SYMBOL(sf_2);
+
+noinline u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c)
+{
+	return a->a + b->b + c->c;
+}
+EXPORT_SYMBOL(sf_3);
+
+noinline u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return a->a + b->b + c->c + d->d;
+}
+EXPORT_SYMBOL(sf_4);
+
+noinline u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e)
+{
+	return a->a + b->b + c->c + d->d + e->e;
+}
+EXPORT_SYMBOL(sf_5);
+
+noinline u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f;
+}
+EXPORT_SYMBOL(sf_6);
+
+noinline u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f, struct s7 *g)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f + g->g;
+}
+EXPORT_SYMBOL(sf_7);
+
+noinline u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f, struct s7 *g, struct s8 *h)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f + g->g + h->h;
+}
+EXPORT_SYMBOL(sf_8);
+
+/*
+ * Value-nested functions. Each reads its own field0 and forwards the address of
+ * every nested member into the matching stf_k, so the whole recursive tower is
+ * walked and each nesting level is a distinct instrumented struct-pointer arg.
+ */
+noinline u64 stf_1(struct st1 *p) { return p->field0; }
+EXPORT_SYMBOL(stf_1);
+
+noinline u64 stf_2(struct st2 *p)
+{
+	return p->field0 + stf_1(&p->field1);
+}
+EXPORT_SYMBOL(stf_2);
+
+noinline u64 stf_3(struct st3 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2);
+}
+EXPORT_SYMBOL(stf_3);
+
+noinline u64 stf_4(struct st4 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3);
+}
+EXPORT_SYMBOL(stf_4);
+
+noinline u64 stf_5(struct st5 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4);
+}
+EXPORT_SYMBOL(stf_5);
+
+noinline u64 stf_6(struct st6 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5);
+}
+EXPORT_SYMBOL(stf_6);
+
+noinline u64 stf_7(struct st7 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5) +
+	       stf_6(&p->field6);
+}
+EXPORT_SYMBOL(stf_7);
+
+noinline u64 stf_8(struct st8 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5) +
+	       stf_6(&p->field6) + stf_7(&p->field7);
+}
+EXPORT_SYMBOL(stf_8);
+
+/*
+ * Pointer-linked functions. Each dereferences its own *field0 and forwards each
+ * (already pointer-typed) nested member into the matching stpf_k, following the
+ * heap-linked tower.
+ */
+noinline u64 stpf_1(struct stp1 *p) { return *p->field0; }
+EXPORT_SYMBOL(stpf_1);
+
+noinline u64 stpf_2(struct stp2 *p)
+{
+	return *p->field0 + stpf_1(p->field1);
+}
+EXPORT_SYMBOL(stpf_2);
+
+noinline u64 stpf_3(struct stp3 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2);
+}
+EXPORT_SYMBOL(stpf_3);
+
+noinline u64 stpf_4(struct stp4 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3);
+}
+EXPORT_SYMBOL(stpf_4);
+
+noinline u64 stpf_5(struct stp5 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4);
+}
+EXPORT_SYMBOL(stpf_5);
+
+noinline u64 stpf_6(struct stp6 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5);
+}
+EXPORT_SYMBOL(stpf_6);
+
+noinline u64 stpf_7(struct stp7 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5) +
+	       stpf_6(p->field6);
+}
+EXPORT_SYMBOL(stpf_7);
+
+noinline u64 stpf_8(struct stp8 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5) +
+	       stpf_6(p->field6) + stpf_7(p->field7);
+}
+EXPORT_SYMBOL(stpf_8);
+
+u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+struct s4 sf_ret_struct(struct s1 *a, struct s2 *b);
+
+/* Pointer forwarding: callee receives pointer and passes it to another func */
+noinline u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return a->a + b->b + c->c + d->d;
+}
+EXPORT_SYMBOL(sf_fwd_inner);
+
+noinline u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return sf_fwd_inner(a, b, c, d);
+}
+EXPORT_SYMBOL(sf_fwd);
+
+/* Struct return value */
+noinline struct s4 sf_ret_struct(struct s1 *a, struct s2 *b)
+{
+	struct s4 ret = { .a = a->a, .b = b->a, .c = b->b, .d = a->a + b->b };
+
+	return ret;
+}
+EXPORT_SYMBOL(sf_ret_struct);
+
+/* Allocator shims so run_stp8() can build the pointer tree with either API. */
+static void *t_kmalloc(size_t n) { return kmalloc(n, GFP_KERNEL); }
+static void *t_vmalloc(size_t n) { return vmalloc(n); }
+static void t_kfree(void *p) { kfree(p); }
+static void t_vfree(void *p) { vfree(p); }
+
+/*
+ * Build the pointer-linked stp8 tower with @alloc (each node separately
+ * allocated), run stpf_8() over it, then free every node with @fr. Sub-nodes
+ * are shared (a DAG); each unique allocation is freed exactly once.
+ */
+static u64 run_stp8(void *(*alloc)(size_t), void (*fr)(void *))
+{
+	u64 ret = 0;
+	u64 *l1 = alloc(sizeof(u64));
+	u64 *l2 = alloc(sizeof(u64));
+	u64 *l3 = alloc(sizeof(u64));
+	u64 *l4 = alloc(sizeof(u64));
+	u64 *l5 = alloc(sizeof(u64));
+	u64 *l6 = alloc(sizeof(u64));
+	u64 *l7 = alloc(sizeof(u64));
+	u64 *l8 = alloc(sizeof(u64));
+	struct stp1 *p1 = alloc(sizeof(*p1));
+	struct stp2 *p2 = alloc(sizeof(*p2));
+	struct stp3 *p3 = alloc(sizeof(*p3));
+	struct stp4 *p4 = alloc(sizeof(*p4));
+	struct stp5 *p5 = alloc(sizeof(*p5));
+	struct stp6 *p6 = alloc(sizeof(*p6));
+	struct stp7 *p7 = alloc(sizeof(*p7));
+	struct stp8 *p8 = alloc(sizeof(*p8));
+
+	if (l1 && l2 && l3 && l4 && l5 && l6 && l7 && l8 &&
+	    p1 && p2 && p3 && p4 && p5 && p6 && p7 && p8) {
+		*l1 = 0x11; *l2 = 0x22; *l3 = 0x33; *l4 = 0x44;
+		*l5 = 0x55; *l6 = 0x66; *l7 = 0x77; *l8 = 0x88;
+
+		p1->field0 = l1;
+		p2->field0 = l2; p2->field1 = p1;
+		p3->field0 = l3; p3->field1 = p1; p3->field2 = p2;
+		p4->field0 = l4; p4->field1 = p1; p4->field2 = p2;
+		p4->field3 = p3;
+		p5->field0 = l5; p5->field1 = p1; p5->field2 = p2;
+		p5->field3 = p3; p5->field4 = p4;
+		p6->field0 = l6; p6->field1 = p1; p6->field2 = p2;
+		p6->field3 = p3; p6->field4 = p4; p6->field5 = p5;
+		p7->field0 = l7; p7->field1 = p1; p7->field2 = p2;
+		p7->field3 = p3; p7->field4 = p4; p7->field5 = p5;
+		p7->field6 = p6;
+		p8->field0 = l8; p8->field1 = p1; p8->field2 = p2;
+		p8->field3 = p3; p8->field4 = p4; p8->field5 = p5;
+		p8->field6 = p6; p8->field7 = p7;
+
+		ret = stpf_8(p8);
+	}
+
+	fr(p8); fr(p7); fr(p6); fr(p5); fr(p4); fr(p3); fr(p2); fr(p1);
+	fr(l8); fr(l7); fr(l6); fr(l5); fr(l4); fr(l3); fr(l2); fr(l1);
+	return ret;
+}
+
+static struct dentry *test_dir;
+
+static ssize_t trigger_write(struct file *f, const char __user *buf,
+			     size_t count, loff_t *ppos)
+{
+	struct s1 v1 = { .a = 0x11 };
+	struct s2 v2 = { .a = 0x11, .b = 0x22 };
+	struct s3 v3 = { .a = 0x11, .b = 0x22, .c = 0x33 };
+	struct s4 v4 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44 };
+	struct s5 v5 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55 };
+	struct s6 v6 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66 };
+	struct s7 v7 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66, .g = 0x77 };
+	struct s8 v8 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66, .g = 0x77, .h = 0x88 };
+
+	/* Recursively (value) nested values: each embeds all the smaller ones. */
+	struct st1 t1 = { .field0 = 0x11 };
+	struct st2 t2 = { .field0 = 0x22, .field1 = t1 };
+	struct st3 t3 = { .field0 = 0x33, .field1 = t1, .field2 = t2 };
+	struct st4 t4 = { .field0 = 0x44, .field1 = t1, .field2 = t2,
+			  .field3 = t3 };
+	struct st5 t5 = { .field0 = 0x55, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4 };
+	struct st6 t6 = { .field0 = 0x66, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4, .field5 = t5 };
+	struct st7 t7 = { .field0 = 0x77, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4, .field5 = t5,
+			  .field6 = t6 };
+	u64 sum = 0;
+
+	/* Flat struct tests: sf_N takes N struct pointer args */
+	sum += sf_1(&v1);
+	sum += sf_2(&v1, &v2);
+	sum += sf_3(&v1, &v2, &v3);
+	sum += sf_4(&v1, &v2, &v3, &v4);
+	sum += sf_5(&v1, &v2, &v3, &v4, &v5);
+	sum += sf_6(&v1, &v2, &v3, &v4, &v5, &v6);
+	sum += sf_7(&v1, &v2, &v3, &v4, &v5, &v6, &v7);
+	sum += sf_8(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8);
+
+	/* Value-nested struct tests (on-stack) */
+	sum += stf_1(&t1);
+	sum += stf_2(&t2);
+	sum += stf_3(&t3);
+	sum += stf_4(&t4);
+	sum += stf_5(&t5);
+	sum += stf_6(&t6);
+	sum += stf_7(&t7);
+	/*
+	 * st8 is 1 KiB; keeping it on the stack alongside t1..t7 blows the 2048-byte
+	 * frame limit (-Wframe-larger-than). Build it on the heap (member-wise, so no
+	 * 1 KiB compound-literal temporary lands on the stack either).
+	 */
+	{
+		struct st8 *t8 = kmalloc(sizeof(*t8), GFP_KERNEL);
+
+		if (t8) {
+			t8->field0 = 0x88;
+			t8->field1 = t1;
+			t8->field2 = t2;
+			t8->field3 = t3;
+			t8->field4 = t4;
+			t8->field5 = t5;
+			t8->field6 = t6;
+			t8->field7 = t7;
+			sum += stf_8(t8);
+			kfree(t8);
+		}
+	}
+
+	/* Dynamic allocation: pointer-linked stp8, each node separately alloc'd */
+	sum += run_stp8(t_kmalloc, t_kfree);	/* heap/slab */
+	sum += run_stp8(t_vmalloc, t_vfree);	/* vmalloc address space */
+
+	/* Pointer forwarding: sf_fwd receives pointers and forwards to inner */
+	sum += sf_fwd(&v1, &v2, &v3, &v4);
+
+	/* Struct return value */
+	{
+		struct s4 ret = sf_ret_struct(&v1, &v2);
+
+		sum += ret.a + ret.b + ret.c + ret.d;
+	}
+
+	/* Keep every call above from being optimised away (sum is otherwise dead). */
+	OPTIMIZER_HIDE_VAR(sum);
+	return count;
+}
+
+static const struct file_operations trigger_fops = {
+	.write = trigger_write,
+};
+
+static int __init eight_struct_args_init(void)
+{
+	test_dir = debugfs_create_dir("kcov_dataflow_test", NULL);
+	debugfs_create_file("trigger_struct", 0200, test_dir, NULL,
+			    &trigger_fops);
+	return 0;
+}
+
+static void __exit eight_struct_args_exit(void)
+{
+	debugfs_remove_recursive(test_dir);
+}
+
+module_init(eight_struct_args_init);
+module_exit(eight_struct_args_exit);
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile
new file mode 100644
index 0000000000000..3017a24774051
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := eight_struct_args_rust.o
+KCOV_DATAFLOW_eight_struct_args_rust.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst
new file mode 100644
index 0000000000000..06e8f8070f6c2
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst
@@ -0,0 +1,11 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: eight_struct_args_rust
+===============================================
+
+Rust equivalent of eight_struct_args_c (rsf_*, rstf_*, rstpf_* with
+``#[no_mangle]``), built only with CONFIG_RUST=y. Opted in with
+``KCOV_DATAFLOW_eight_struct_args_rust.o := y``::
+
+  ./test_modules.py -t eight_struct_args_rust
+  ./trigger-view.py eight_struct_args_rust --raw
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs
new file mode 100644
index 0000000000000..e5cc3cb87591e
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs
@@ -0,0 +1,646 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Verify kcov_dataflow captures struct pointer arguments with automatic
+//! field expansion for Rust #[repr(C)] structs.
+//!
+//! Rust equivalent of eight_struct_args_c. Two families are exercised:
+//!   - Flat structs S1..S8 (1-8 u64 members) via rsf_N.
+//!   - Recursively (value) nested structs St1..St8, where StN embeds every
+//!     smaller struct by value:
+//!         St1 = { field0 }
+//!         St2 = { field0, field1: St1 }              // { v, {v} }
+//!         StN = { field0, field1: St1, ..., field(N-1): St(N-1) }
+//!     so St8 is eight levels deep along its St7 chain. Each rstf_N reads its
+//!     own field0 and forwards each nested member's address into rstf_k.
+//!   - Pointer-linked nested structs Stp1..Stp8, where every member is a raw
+//!     pointer to a separately allocated object:
+//!         Stp1 = { field0: *const u64 }
+//!         StpN = { field0: *const u64, field1: *const Stp1, ... }
+//!     The heap (KBox) test builds this tower and follows it via rstpf_N.
+//!
+//! Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct_rust to invoke.
+
+#![allow(missing_docs)]
+
+use kernel::prelude::*;
+use kernel::alloc::KBox;
+use kernel::c_str;
+
+module !{
+	type:EightStructArgsRust,
+	name: "eight_struct_args_rust",
+	authors: ["kcov-dataflow"],
+	description: "Struct field expansion test for kcov_dataflow (Rust)",
+	license: "GPL",
+}
+#[repr(C)]
+pub struct S1 {
+	pub a : u64
+}
+#[repr(C)]
+pub struct S2 {
+	pub a : u64, pub b : u64
+}
+#[repr(C)]
+pub struct S3 {
+	pub a : u64, pub b : u64, pub c : u64
+}
+#[repr(C)]
+pub struct S4 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64
+}
+#[repr(C)]
+pub struct S5 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64
+}
+#[repr(C)]
+pub struct S6 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64
+}
+#[repr(C)]
+pub struct S7 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64, pub g : u64
+}
+#[repr(C)]
+pub struct S8 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64, pub g : u64, pub h : u64
+}
+// Recursively nested: StN = { field0, field1: St1, ..., field(N-1): St(N-1) }.
+// Copy so a smaller value can be embedded into every larger one.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St1 {
+	pub field0 : u64
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St2 {
+	pub field0 : u64, pub field1 : St1
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St3 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St4 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St5 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St6 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St7 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5, pub field6 : St6
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St8 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5, pub field6 : St6,
+		pub field7 : St7
+}
+// Pointer-linked nested: every member is a raw pointer to a separately
+// allocated object. StpN = { field0: *const u64, field1: *const Stp1, ... }.
+#[repr(C)]
+pub struct Stp1 {
+	pub field0 : *const u64
+}
+#[repr(C)]
+pub struct Stp2 {
+	pub field0 : *const u64, pub field1 : *const Stp1
+}
+#[repr(C)]
+pub struct Stp3 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2
+}
+#[repr(C)]
+pub struct Stp4 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3
+}
+#[repr(C)]
+pub struct Stp5 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4
+}
+#[repr(C)]
+pub struct Stp6 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5
+}
+#[repr(C)]
+pub struct Stp7 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5,
+		pub field6 : *const Stp6
+}
+#[repr(C)]
+pub struct Stp8 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5,
+		pub field6 : *const Stp6, pub field7 : *const Stp7
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_1(a : *const S1) -> u64
+{
+	unsafe
+	{
+		(*a).a
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_2(a : *const S1, b : *const S2) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_4(a : *const S1, b : *const S2, c : *const S3,
+			d : *const S4) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_8(a : *const S1, b : *const S2, c : *const S3,
+			d : *const S4, e : *const S5, f : *const S6,
+			g : *const S7, h : *const S8) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d + (*e).e + (*f).f + (*g).g +
+			(*h).h
+	}
+}
+
+// Recursively nested: each reads its own field0 and forwards every nested
+// member's address into the matching rstf_k, walking the whole tower.
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_1(p : *const St1) -> u64
+{
+	unsafe
+	{
+		(*p).field0
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_2(p : *const St2) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_3(p : *const St3) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_4(p : *const St4) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_5(p : *const St5) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_6(p : *const St6) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_7(p : *const St7) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5) + rstf_6(&(*p).field6)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_8(p : *const St8) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5) + rstf_6(&(*p).field6) +
+			rstf_7(&(*p).field7)
+	}
+}
+
+// Pointer-linked: each dereferences its own *field0 and forwards each
+// (already pointer-typed) nested member into the matching rstpf_k.
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_1(p : *const Stp1) -> u64
+{
+	unsafe
+	{
+		*(*p).field0
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_2(p : *const Stp2) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_3(p : *const Stp3) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_4(p : *const Stp4) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_5(p : *const Stp5) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_6(p : *const Stp6) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_7(p : *const Stp7) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5) + rstpf_6((*p).field6)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_8(p : *const Stp8) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5) + rstpf_6((*p).field6) +
+			rstpf_7((*p).field7)
+	}
+}
+
+// Build the pointer-linked Stp8 tower with KBox (each node its own allocation),
+// run rstpf_8 over it, and return the sum. The KBoxes own the storage and hold
+// raw pointers into their siblings; everything is freed when they drop at the
+// end of this function. `?` frees any already-allocated KBoxes on OOM.
+fn build_and_run_stp8() -> Result<u64>
+{
+	let l1 = KBox::new (0x11u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l2 = KBox::new (0x22u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l3 = KBox::new (0x33u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l4 = KBox::new (0x44u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l5 = KBox::new (0x55u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l6 = KBox::new (0x66u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l7 = KBox::new (0x77u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l8 = KBox::new (0x88u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+
+	let p1 = KBox::new (Stp1{ field0: &*l1 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p2 = KBox::new (Stp2{ field0: &*l2, field1: &*p1 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p3 = KBox::new (Stp3{ field0: &*l3, field1: &*p1, field2: &*p2 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p4 = KBox::new (
+		Stp4{ field0: &*l4, field1: &*p1, field2: &*p2, field3: &*p3 },
+		kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p5 = KBox::new (Stp5{
+		field0: &*l5,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p6 = KBox::new (Stp6{
+		field0: &*l6,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p7 = KBox::new (Stp7{
+		field0: &*l7,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5,
+		field6: &*p6
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p8 = KBox::new (Stp8{
+		field0: &*l8,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5,
+		field6: &*p6,
+		field7: &*p7
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+
+	Ok(rstpf_8(&*p8))
+}
+
+/* Pointer forwarding: receives pointers and passes to inner */
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_fwd_inner(a : *const S1, b : *const S2, c : *const S3,
+				d : *const S4) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_fwd(a : *const S1, b : *const S2, c : *const S3,
+			  d : *const S4) -> u64{ rsf_fwd_inner(a, b, c, d) }
+
+/* Struct return value */
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_ret_struct(a : *const S1, b : *const S2)
+	->S4
+{
+	unsafe
+	{
+		S4
+		{
+a:
+			(*a).a, b : (*b).a, c : (*b).b, d : (*a).a + (*b).b
+		}
+	}
+}
+
+unsafe extern "C" fn write_handler(_file : *mut kernel::bindings::file,
+				   _buf : *const core::ffi::c_char,
+				   count : usize,
+				   _ppos : *mut kernel::bindings::loff_t, )
+	-> kernel::ffi::c_long
+{
+	let v1 = S1{ a: 0x11 };
+	let v2 = S2{ a: 0x11, b: 0x22 };
+	let v3 = S3{ a: 0x11, b: 0x22, c: 0x33 };
+	let v4 = S4{ a: 0x11, b: 0x22, c: 0x33, d: 0x44 };
+	let v5 = S5{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55 };
+	let v6 = S6{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66 };
+	let v7 =
+	S7{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66, g: 0x77 };
+	let v8 = S8{
+		a: 0x11,
+		b: 0x22,
+		c: 0x33,
+		d: 0x44,
+		e: 0x55,
+		f: 0x66,
+		g: 0x77,
+		h: 0x88
+	};
+
+	// Recursively nested values: each embeds all the smaller ones (Copy).
+	let t1 = St1{ field0: 0x11 };
+	let t2 = St2{ field0: 0x22, field1: t1 };
+	let t3 = St3{ field0: 0x33, field1: t1, field2: t2 };
+	let t4 = St4{ field0: 0x44, field1: t1, field2: t2, field3: t3 };
+	let t5 =
+	St5{ field0: 0x55, field1: t1, field2: t2, field3: t3, field4: t4 };
+	let t6 = St6{
+		field0: 0x66,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5
+	};
+	let t7 = St7{
+		field0: 0x77,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5,
+		field6: t6
+	};
+	let t8 = St8{
+		field0: 0x88,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5,
+		field6: t6,
+		field7: t7
+	};
+
+	let mut sum : u64 = 0;
+	sum = sum.wrapping_add(rsf_1(&v1 as *const S1));
+	sum = sum.wrapping_add(rsf_2(&v1 as *const S1, &v2 as *const S2));
+	sum = sum.wrapping_add(rsf_4(&v1 as *const S1, &v2 as *const S2,
+				     &v3 as *const S3, &v4 as *const S4));
+	sum = sum.wrapping_add(rsf_8(&v1 as *const S1, &v2 as *const S2,
+				     &v3 as *const S3, &v4 as *const S4,
+				     &v5 as *const S5, &v6 as *const S6,
+				     &v7 as *const S7, &v8 as *const S8));
+
+	// Recursively nested struct tests
+	sum = sum.wrapping_add(rstf_1(&t1 as *const St1));
+	sum = sum.wrapping_add(rstf_2(&t2 as *const St2));
+	sum = sum.wrapping_add(rstf_3(&t3 as *const St3));
+	sum = sum.wrapping_add(rstf_4(&t4 as *const St4));
+	sum = sum.wrapping_add(rstf_5(&t5 as *const St5));
+	sum = sum.wrapping_add(rstf_6(&t6 as *const St6));
+	sum = sum.wrapping_add(rstf_7(&t7 as *const St7));
+	sum = sum.wrapping_add(rstf_8(&t8 as *const St8));
+
+	// Pointer forwarding: rsf_fwd receives and passes to rsf_fwd_inner
+	sum = sum.wrapping_add(rsf_fwd(&v1 as *const S1, &v2 as *const S2,
+				       &v3 as *const S3, &v4 as *const S4));
+
+	// Struct return value
+	let ret = rsf_ret_struct(&v1 as *const S1, &v2 as *const S2);
+	sum = sum.wrapping_add(ret.a + ret.b + ret.c + ret.d);
+
+	// Dynamic allocation: pointer-linked Stp8 tower (each node its own KBox)
+	if let
+		Ok(s) = build_and_run_stp8()
+		{
+			sum = sum.wrapping_add(s);
+		}
+
+	core::hint::black_box(sum);
+	count as kernel::ffi::c_long
+}
+
+#[repr(transparent)]
+struct SyncFops(kernel::bindings::file_operations);
+unsafe impl Sync for SyncFops
+{
+}
+
+static FOPS : SyncFops = SyncFops(kernel::bindings::file_operations{
+	write: Some(unsafe{ core::mem::transmute(write_handler as *const()) }),
+	..unsafe{ core::mem::zeroed() }
+});
+
+struct EightStructArgsRust {
+	dir : *mut kernel::bindings::dentry,
+}
+
+impl kernel::Module for EightStructArgsRust
+{
+    fn init(_module: &'static ThisModule) -> Result<Self> {
+        let dir = unsafe {
+            kernel::bindings::debugfs_create_dir(
+                c_str!("kcov_dataflow_test").as_char_ptr(),
+                core::ptr::null_mut(),
+            )
+        };
+        unsafe {
+            kernel::bindings::debugfs_create_file_unsafe(
+                c_str!("trigger_struct_rust").as_char_ptr(),
+                0o222,
+                dir,
+                core::ptr::null_mut(),
+                &FOPS.0,
+            )
+        };
+        Ok(Self { dir })
+}
+}
+
+impl Drop for EightStructArgsRust
+{
+	fn drop(&mut self)
+	{
+		unsafe{ kernel::bindings::debugfs_remove(self.dir) };
+	}
+}
+
+unsafe impl Send for EightStructArgsRust
+{
+}
+unsafe impl Sync for EightStructArgsRust
+{
+}
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile
new file mode 100644
index 0000000000000..d2a0261070b1c
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := rust_ffi_contract.o
+KCOV_DATAFLOW_rust_ffi_contract.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst
new file mode 100644
index 0000000000000..291621fa799cd
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: rust_ffi_contract
+==========================================
+
+FFI contract violation detection: ffi_alloc_buf() returns 0 but leaves
+alloc->buffer NULL, and ffi_check_result() receives that NULL. The test
+checks the expanded ``struct ffi_alloc`` at both boundaries, the scalar
+arguments (256, 16, 1), the 0 return and the -EFAULT from the checker.
+Opted in with ``KCOV_DATAFLOW_rust_ffi_contract.o := y``::
+
+  ./test_modules.py -t rust_ffi_contract
+  ./trigger-view.py rust_ffi_contract -C 8
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
new file mode 100644
index 0000000000000..071bd25dfec11
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * rust_ffi_contract.c - Demonstrates kcov_dataflow detecting an FFI
+ * contract violation at a function boundary.
+ *
+ * The pattern: caller passes a struct pointer to callee. Callee's
+ * contract says "returns 0 implies out->buffer is valid". A bug in
+ * the async path returns 0 but leaves buffer=NULL.
+ *
+ * kcov_dataflow captures:
+ *   [ENTRY] ffi_alloc_buf(alloc={.buffer=NULL, .data_size=0}, 256, 16, 1)
+ *   [RET]   ffi_alloc_buf() = 0
+ *   [ENTRY] ffi_check_result(alloc={.buffer=NULL, .data_size=0x110, ...})
+ *                             ^ proves contract violated
+ *   [RET]   ffi_check_result() = -EFAULT
+ *
+ * Write to /sys/kernel/debug/kcov_dataflow_test/rust_ffi_trigger to run.
+ */
+#include <linux/module.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("FFI contract violation detection via kcov_dataflow");
+
+struct ffi_alloc {
+	void *buffer;
+	u64 data_size;
+	u32 free_async;
+	u32 flags;
+};
+
+/* Prototypes */
+int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,
+		  u64 offsets_size, int is_async);
+int ffi_check_result(struct ffi_alloc *alloc);
+
+/*
+ * Callee with contract: returns 0 implies alloc->buffer is valid.
+ * BUG: async path with free_async==0 returns 0 but buffer stays NULL.
+ */
+noinline int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,
+			   u64 offsets_size, int is_async)
+{
+	/*
+	 * data_size + offsets_size is used on every path so that the compiler
+	 * keeps offsets_size alive (an unused parameter is dropped at -O2 and
+	 * callers then pass poison, leaving nothing to trace).
+	 */
+	if (!is_async) {
+		alloc->buffer = kmalloc(data_size + offsets_size, GFP_KERNEL);
+		if (!alloc->buffer)
+			return -ENOMEM;
+		return 0;
+	}
+	/* BUG: returns success but buffer is NULL when pool empty */
+	if (alloc->free_async == 0) {
+		alloc->buffer = NULL;
+		alloc->data_size = data_size + offsets_size;
+		return 0; /* contract violation */
+	}
+	alloc->buffer = kmalloc(data_size + offsets_size, GFP_KERNEL);
+	alloc->free_async--;
+	return 0;
+}
+EXPORT_SYMBOL(ffi_alloc_buf);
+
+/* Caller that trusts the contract */
+noinline int ffi_check_result(struct ffi_alloc *alloc)
+{
+	if (!alloc->buffer) {
+		pr_err("ffi_contract: VIOLATION detected - buffer is NULL after success\n");
+		return -EFAULT;
+	}
+	kfree(alloc->buffer);
+	return 0;
+}
+EXPORT_SYMBOL(ffi_check_result);
+
+static struct dentry *test_dir;
+
+static ssize_t rust_ffi_trigger_write(struct file *f, const char __user *buf,
+				 size_t count, loff_t *ppos)
+{
+	struct ffi_alloc alloc = { .buffer = NULL, .data_size = 0,
+				   .free_async = 0, .flags = 0 };
+	int ret;
+
+	/*
+	 * Keep the initializer: the callee provably writes alloc->buffer before
+	 * reading it, so without the barrier the compiler drops the NULL store
+	 * and the ENTRY record would show stack garbage instead of NULL.
+	 */
+	barrier_data(&alloc);
+
+	/* Trigger the bug: is_async=1, free_async=0 */
+	ret = ffi_alloc_buf(&alloc, 256, 16, 1);
+	pr_info("ffi_contract: ffi_alloc_buf returned %d, buffer=%p\n",
+		ret, alloc.buffer);
+
+	if (ret == 0)
+		ffi_check_result(&alloc);
+
+	return count;
+}
+
+static const struct file_operations rust_ffi_trigger_fops = {
+	.write = rust_ffi_trigger_write,
+};
+
+static int __init ffi_contract_init(void)
+{
+	test_dir = debugfs_create_dir("kcov_dataflow_test", NULL);
+	debugfs_create_file("rust_ffi_trigger", 0200, test_dir, NULL,
+			    &rust_ffi_trigger_fops);
+	return 0;
+}
+
+static void __exit ffi_contract_exit(void)
+{
+	debugfs_remove_recursive(test_dir);
+}
+
+module_init(ffi_contract_init);
+module_exit(ffi_contract_exit);
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile
new file mode 100644
index 0000000000000..cb7392a50b1a9
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := rust_kworker_remote.o
+KCOV_DATAFLOW_rust_kworker_remote.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst
new file mode 100644
index 0000000000000..aff597ab67aea
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: rust_kworker_remote
+============================================
+
+Rust module testing kcov_df_remote_start()/kcov_df_remote_stop() from
+kworker context: the trigger queues a work item on system_wq whose three
+phases (populate/update/drain of a CompositeStore of RBTrees) run with
+remote capture on handle 1, which the runner publishes with
+KCOV_DF_REMOTE_ENABLE. Built only with CONFIG_RUST=y::
+
+  ./test_modules.py -t rust_kworker_remote
+  ./trigger-view.py rust_kworker_remote --remote
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs
new file mode 100644
index 0000000000000..65c5722c383cc
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Test kcov_df_remote_start/stop from kworker context.
+//!
+//! A composite struct holds three RBTrees (simulating RBTree/XArray/maple_tree
+//! workloads). Three work phases run on system_wq:
+//!   Phase 1 (populate): fill all three trees
+//!   Phase 2 (update): insert new values, read existing, overwrite
+//!   Phase 3 (drain): remove all entries
+//!
+//! User space publishes a buffer with KCOV_DF_REMOTE_ENABLE, writes to
+//! /sys/kernel/debug/kcov_dataflow_test/trigger_kworker_remote, then reads
+//! the captured records.
+
+#![allow(missing_docs)]
+
+use kernel::prelude::*;
+use kernel::sync::{Arc, Completion};
+use kernel::workqueue::{self, impl_has_work, new_work, Work, WorkItem};
+use kernel::rbtree::RBTree;
+use kernel::c_str;
+
+module! {
+    type: RustKworkerRemote,
+    name: "rust_kworker_remote",
+    authors: ["kcov-dataflow"],
+    description: "Test kcov_df_remote capturing from kworker (RBTree composite)",
+    license: "GPL",
+}
+
+// Extern bindings for kcov_dataflow remote API (kernel/kcov_dataflow.c)
+unsafe extern "C" {
+    fn kcov_df_remote_start(handle: u64);
+    fn kcov_df_remote_stop();
+}
+
+/// Composite data structure: three trees with different key ranges.
+/// Simulates a real driver managing multiple lookup tables.
+struct CompositeStore {
+    /// Primary index (keys 0..N)
+    primary: RBTree<u64, u64>,
+    /// Secondary/auxiliary index (keys 100..N)
+    aux: RBTree<u64, u64>,
+    /// Scratch/temp space (keys 200..N)
+    scratch: RBTree<u64, u64>,
+}
+
+impl CompositeStore {
+    fn new() -> Self {
+        Self {
+            primary: RBTree::new(),
+            aux: RBTree::new(),
+            scratch: RBTree::new(),
+        }
+    }
+
+    /// Phase 1: populate all three trees with initial data.
+    #[inline(never)]
+    fn populate(&mut self) -> Result {
+        for i in 0u64..8 {
+            self.primary.try_create_and_insert(i, i * 0x1111, GFP_KERNEL)?;
+        }
+        for i in 100u64..108 {
+            self.aux.try_create_and_insert(i, i * 0x2222, GFP_KERNEL)?;
+        }
+        for i in 200u64..208 {
+            self.scratch.try_create_and_insert(i, i * 0x3333, GFP_KERNEL)?;
+        }
+        Ok(())
+    }
+
+    /// Phase 2: insert more, read existing, overwrite some.
+    #[inline(never)]
+    fn update(&mut self) -> Result {
+        // Insert new entries into primary
+        for i in 8u64..12 {
+            self.primary.try_create_and_insert(i, i * 0x4444, GFP_KERNEL)?;
+        }
+        // Read from aux (get passes &K which is a struct arg)
+        for i in 100u64..108 {
+            let _ = self.aux.get(&i);
+        }
+        // Overwrite scratch entries
+        for i in 200u64..204 {
+            self.scratch.remove(&i);
+            self.scratch.try_create_and_insert(i, i * 0x5555, GFP_KERNEL)?;
+        }
+        Ok(())
+    }
+
+    /// Phase 3: drain all trees.
+    #[inline(never)]
+    fn drain(&mut self) {
+        while let Some(c) = self.primary.cursor_front_mut() {
+            c.remove_current();
+        }
+        while let Some(c) = self.aux.cursor_front_mut() {
+            c.remove_current();
+        }
+        while let Some(c) = self.scratch.cursor_front_mut() {
+            c.remove_current();
+        }
+    }
+}
+
+/// Work item that runs three phases in kworker context with remote capture.
+#[pin_data]
+struct RemoteWork {
+    #[pin]
+    work: Work<RemoteWork>,
+    #[pin]
+    done: Completion,
+}
+
+impl_has_work! {
+    impl HasWork<Self> for RemoteWork { self.work }
+}
+
+impl RemoteWork {
+    fn new() -> Result<Arc<Self>> {
+        Arc::pin_init(pin_init!(RemoteWork {
+            work <- new_work!("RemoteWork::work"),
+            done <- Completion::new(),
+        }), GFP_KERNEL)
+    }
+}
+
+impl WorkItem for RemoteWork {
+    type Pointer = Arc<RemoteWork>;
+
+    fn run(this: Arc<RemoteWork>) {
+        // Enable remote kcov_dataflow capture for this kworker task.
+        // SAFETY: FFI call to exported kernel symbol; no-op if no buffer published.
+        // Handle 1 matches what trigger-view.py passes via KCOV_DF_REMOTE_ENABLE.
+        unsafe { kcov_df_remote_start(1) };
+
+        let mut store = CompositeStore::new();
+        let _ = store.populate();
+        let _ = store.update();
+        store.drain();
+
+        // SAFETY: FFI call to exported kernel symbol; disables capture.
+        unsafe { kcov_df_remote_stop() };
+
+        this.done.complete_all();
+    }
+}
+
+// --- Debugfs trigger (same raw pattern as eight_struct_args_rust) ---
+
+unsafe extern "C" fn write_handler(
+    _file: *mut kernel::bindings::file,
+    _buf: *const core::ffi::c_char,
+    count: usize,
+    _ppos: *mut kernel::bindings::loff_t,
+) -> kernel::ffi::c_long {
+    let work = match RemoteWork::new() {
+        Ok(w) => w,
+        Err(_) => return -(kernel::bindings::ENOMEM as kernel::ffi::c_long),
+    };
+    let waiter = work.clone();
+    let _ = workqueue::system().enqueue(work);
+    waiter.done.wait_for_completion();
+    count as kernel::ffi::c_long
+}
+
+#[repr(transparent)]
+struct SyncFops(kernel::bindings::file_operations);
+unsafe impl Sync for SyncFops {}
+
+static FOPS: SyncFops = SyncFops(kernel::bindings::file_operations {
+    write: Some(unsafe { core::mem::transmute(write_handler as *const ()) }),
+    ..unsafe { core::mem::zeroed() }
+});
+
+struct RustKworkerRemote {
+    dir: *mut kernel::bindings::dentry,
+}
+
+impl kernel::Module for RustKworkerRemote {
+    fn init(_module: &'static ThisModule) -> Result<Self> {
+        let dir = unsafe {
+            kernel::bindings::debugfs_create_dir(
+                c_str!("kcov_dataflow_test").as_char_ptr(),
+                core::ptr::null_mut(),
+            )
+        };
+        unsafe {
+            kernel::bindings::debugfs_create_file_unsafe(
+                c_str!("trigger_kworker_remote").as_char_ptr(),
+                0o222,
+                dir,
+                core::ptr::null_mut(),
+                &FOPS.0,
+            )
+        };
+        Ok(Self { dir })
+    }
+}
+
+impl Drop for RustKworkerRemote {
+    fn drop(&mut self) {
+        unsafe { kernel::bindings::debugfs_remove(self.dir) };
+    }
+}
+
+unsafe impl Send for RustKworkerRemote {}
+unsafe impl Sync for RustKworkerRemote {}
diff --git a/tools/testing/selftests/kcov_dataflow/settings b/tools/testing/selftests/kcov_dataflow/settings
new file mode 100644
index 0000000000000..694d70710ff08
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/kcov_dataflow/test_modules.py b/tools/testing/selftests/kcov_dataflow/test_modules.py
new file mode 100755
index 0000000000000..13cb706a06ff6
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/test_modules.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+test_modules.py - run the kcov_dataflow test modules, one KTAP test each.
+
+Every module is loaded, triggered with recording active and unloaded by
+trigger-view.py's run_capture(). The records that belong to the module are
+then compared with the values its trigger function passes and returns, so a
+test passes only when the instrumented arguments, struct field expansions
+and return values came back intact through the kcov_dataflow buffer. The
+module's call tree is echoed as KTAP diagnostics.
+
+    ./test_modules.py                 # all modules
+    ./test_modules.py -t rust_ffi_contract -C 8 --vmlinux vmlinux
+
+Modules that were not built (no CONFIG_RUST, no toolchain) are reported as
+SKIP; a kernel without /sys/kernel/debug/kcov_dataflow skips everything.
+"""
+import argparse
+import contextlib
+import importlib.util
+import io
+import os
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(HERE, "..", "kselftest"))
+import ksft  # noqa: E402
+
+
+def _load_trigger_view():
+    spec = importlib.util.spec_from_file_location(
+        "trigger_view", os.path.join(HERE, "trigger-view.py"))
+    mod = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(mod)
+    return mod
+
+
+tv = _load_trigger_view()
+
+
+class Check:
+    """Collects expectation failures for one module."""
+
+    def __init__(self):
+        self.failures = []
+
+    def eq(self, what, got, want):
+        if got != want:
+            self.failures.append(f"{what}: got {fmt(got)}, want {fmt(want)}")
+
+    def true(self, what, cond):
+        if not cond:
+            self.failures.append(what)
+
+
+def fmt(v):
+    if isinstance(v, list):
+        return "[" + ", ".join(fmt(x) for x in v) + "]"
+    if isinstance(v, int):
+        return f"0x{v:x}"
+    return str(v)
+
+
+def entries(cap, recs, func):
+    return [r for r in recs if r["type"] == tv.DF_TYPE_ENTRY and func in cap.funcs(r)]
+
+
+def rets(cap, recs, func):
+    return [r["val"] for r in recs if r["type"] == tv.DF_TYPE_RET and func in cap.funcs(r)]
+
+
+def flat_sum(n):
+    """sf_n() returns a->a + b->b + ... over s1..sn: 0x11 + 0x22 + ..."""
+    return sum(0x11 * k for k in range(1, n + 1))
+
+
+def nested_sum(n, _memo={}):
+    """
+    stf_n()/stpf_n() return field0 (0x11 * n) plus the recursive sums of the
+    embedded st1..st(n-1); the same values are used for the value-nested and
+    the pointer-linked towers.
+    """
+    if n not in _memo:
+        _memo[n] = 0x11 * n + sum(nested_sum(k) for k in range(1, n))
+    return _memo[n]
+
+
+def check_struct_family(cap, recs, c, p, flat_ns, stpf8_runs):
+    """
+    Shared expectations for eight_struct_args_c (p="") and
+    eight_struct_args_rust (p="r"): @flat_ns are the sf_N called by the
+    trigger, @stpf8_runs how often the pointer-linked tower is walked.
+    """
+    for n in flat_ns:
+        ents = entries(cap, recs, f"{p}sf_{n}")
+        c.true(f"{p}sf_{n}: ENTRY records", bool(ents))
+        for k in range(n):
+            # arg k is a struct s(k+1) * whose fields are 0x11, 0x22, ...
+            got = [r["vals"] for r in ents if r["arg_idx"] == k]
+            c.true(f"{p}sf_{n} arg[{k}]: ENTRY record", bool(got))
+            for vals in got:
+                c.eq(f"{p}sf_{n} arg[{k}] expanded fields", vals,
+                     [0x11 * (j + 1) for j in range(k + 1)])
+        # rustc may alias identical bodies (rsf_1 == rstf_1), so the RET
+        # list can carry the alias's calls too: check every value.
+        got = rets(cap, recs, f"{p}sf_{n}")
+        c.true(f"{p}sf_{n} RET values all {fmt(flat_sum(n))}: {fmt(got)}",
+               bool(got) and all(v == flat_sum(n) for v in got))
+
+    for fam, calls in ((f"{p}stf", 1), (f"{p}stpf", stpf8_runs)):
+        for n in range(1, 9):
+            got = rets(cap, recs, f"{fam}_{n}")
+            c.true(f"{fam}_{n}: RET records", bool(got))
+            c.true(f"{fam}_{n} RET values all {fmt(nested_sum(n))}: {fmt(got)}",
+                   all(v == nested_sum(n) for v in got))
+        c.eq(f"{fam}_8 RET count", len(rets(cap, recs, f"{fam}_8")), calls)
+
+    for f in (f"{p}sf_fwd", f"{p}sf_fwd_inner"):
+        c.eq(f"{f} RET", rets(cap, recs, f), [flat_sum(4)])
+
+    c.true(f"{p}sf_ret_struct: ENTRY records",
+           bool(entries(cap, recs, f"{p}sf_ret_struct")))
+    c.true(f"{p}sf_ret_struct: RET record",
+           bool(rets(cap, recs, f"{p}sf_ret_struct")))
+
+
+def check_eight_struct_args_c(cap, recs, c):
+    check_struct_family(cap, recs, c, "", range(1, 9), stpf8_runs=2)
+
+
+def check_eight_struct_args_rust(cap, recs, c):
+    check_struct_family(cap, recs, c, "r", (1, 2, 4, 8), stpf8_runs=1)
+
+
+def check_rust_ffi_contract(cap, recs, c):
+    """
+    ffi_alloc_buf(&alloc = {NULL, 0, 0, 0}, 256, 16, is_async=1) records
+    data_size + offsets_size and returns 0 without filling alloc->buffer;
+    ffi_check_result() then sees {NULL, 0x110, 0, 0}. The records must show
+    the violated contract at both boundaries.
+    """
+    ents = entries(cap, recs, "ffi_alloc_buf")
+    by_arg = {r["arg_idx"]: r for r in ents}
+    c.eq("ffi_alloc_buf ENTRY arg indexes", sorted(by_arg), [0, 1, 2, 3])
+    if 0 in by_arg:
+        c.eq("ffi_alloc_buf arg[0] struct ffi_alloc fields",
+             by_arg[0]["vals"], [0, 0, 0, 0])
+    if 1 in by_arg:
+        c.eq("ffi_alloc_buf arg[1] data_size", by_arg[1]["val"], 256)
+    if 2 in by_arg:
+        c.eq("ffi_alloc_buf arg[2] offsets_size", by_arg[2]["val"], 16)
+    if 3 in by_arg:
+        c.eq("ffi_alloc_buf arg[3] is_async", by_arg[3]["val"], 1)
+    c.eq("ffi_alloc_buf RET (claims success)", rets(cap, recs, "ffi_alloc_buf"), [0])
+
+    ents = entries(cap, recs, "ffi_check_result")
+    c.true("ffi_check_result: ENTRY record", bool(ents))
+    for r in ents:
+        c.eq("ffi_check_result arg[0] {buffer NULL: contract violated, "
+             "data_size, free_async, flags}", r["vals"], [0, 0x110, 0, 0])
+    got = rets(cap, recs, "ffi_check_result")
+    c.true(f"ffi_check_result RET -EFAULT: {fmt(got)}",
+           len(got) == 1 and got[0] & 0xffffffff == 0xfffffff2)
+
+
+def check_rust_kworker_remote(cap, recs, c):
+    """
+    The trigger only queues a work item and waits; the records come from the
+    kworker that called kcov_df_remote_start(REMOTE_HANDLE). All three phases
+    of CompositeStore must show up (v0-mangled names keep the method names).
+    """
+    c.true("records captured from the kworker", bool(recs))
+    names = set().union(*(cap.funcs(r) for r in recs)) if recs else set()
+    for phase in ("populate", "update", "drain"):
+        c.true(f"CompositeStore::{phase} recorded",
+               any("CompositeStore" in n and phase in n for n in names))
+
+
+TESTS = (
+    ("rust_ffi_contract", False, check_rust_ffi_contract),
+    ("eight_struct_args_c", False, check_eight_struct_args_c),
+    ("eight_struct_args_rust", False, check_eight_struct_args_rust),
+    ("rust_kworker_remote", True, check_rust_kworker_remote),
+)
+
+
+def diag_tree(cap, recs, vmlinux):
+    out = io.StringIO()
+    with contextlib.redirect_stdout(out):
+        tv.print_tree(recs, cap.syms, vmlinux, {}, cap.ko_path,
+                      cap.mod_text_start)
+    for line in out.getvalue().splitlines():
+        ksft.print_msg(line)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
+    parser.add_argument("-t", "--test", action="append",
+                        help="run only this module (repeatable)")
+    parser.add_argument("-C", "--context", type=int, default=0,
+                        help="echo N records before/after each module record")
+    parser.add_argument("--vmlinux", help="vmlinux for addr2line and KASLR")
+    args = parser.parse_args()
+
+    tests = [t for t in TESTS if not args.test or t[0] in args.test]
+    ksft.print_header()
+    ksft.set_plan(len(tests))
+
+    skip_all = None
+    if not os.path.exists(tv.KCOV_DF_PATH):
+        skip_all = f"{tv.KCOV_DF_PATH} not available (CONFIG_KCOV_DATAFLOW_ARGS/RET)"
+    elif os.geteuid() != 0:
+        skip_all = "must run as root"
+
+    vmlinux = tv.find_vmlinux(args.vmlinux)
+    for name, remote, check in tests:
+        if skip_all:
+            ksft.test_result_skip(f"{name}: {skip_all}")
+            continue
+        ko = tv.find_module(name)
+        if not ko:
+            ksft.test_result_skip(f"{name}: {name}.ko not built")
+            continue
+        try:
+            cap = tv.run_capture(ko, remote=remote, vmlinux=vmlinux,
+                                 log=ksft.print_msg)
+        except OSError as e:
+            ksft.test_result_fail(f"{name}: {e}")
+            continue
+
+        recs = cap.module_records()
+        ksft.print_msg(f"{name}: {cap.total_words} words, {len(cap.records)} "
+                       f"records, {len(recs)} from {name} "
+                       f"(kaslr_offset=0x{cap.kaslr_offset:x})")
+        diag_tree(cap, cap.context_records(args.context) if args.context
+                  else recs, vmlinux)
+
+        c = Check()
+        check(cap, recs, c)
+        for f in c.failures:
+            ksft.print_msg(f"FAIL {name}: {f}")
+        ksft.test_result(not c.failures, name)
+
+    ksft.finished()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/testing/selftests/kcov_dataflow/trigger-view.py b/tools/testing/selftests/kcov_dataflow/trigger-view.py
new file mode 100755
index 0000000000000..b17e49da402d7
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/trigger-view.py
@@ -0,0 +1,755 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+trigger-view.py - Load a test module, trigger it with kcov_dataflow
+recording active, then pretty-print the captured records.
+
+Usage:
+    python3 trigger-view.py eight_struct_args_c
+    python3 trigger-view.py rust_ffi_contract --raw -C 8
+    python3 trigger-view.py rust_kworker_remote --remote
+    python3 trigger-view.py <module> --vmlinux vmlinux --kaslr-offset 0x...
+
+run_capture() does the work and is also what test_modules.py drives:
+  1. Opens /sys/kernel/debug/kcov_dataflow, inits and mmaps the buffer
+  2. Loads the module via finit_module() (its init noise is not recorded)
+  3. Enables recording: KCOV_DF_ENABLE for this task, or with --remote
+     KCOV_DF_REMOTE_ENABLE with handle REMOTE_HANDLE, which the module's
+     kworker opens with kcov_df_remote_start(REMOTE_HANDLE)
+  4. Writes the trigger file(s) the module created under TRIGGER_DIR
+  5. Disables recording and unloads the module
+  6. Parses the records (layout: include/uapi/linux/kcov_dataflow.h)
+
+The CLI then prints them as a call tree, or flat with --raw, with kallsyms
+symbol resolution and addr2line source lines (vmlinux / module .ko).
+
+Recorded PCs have the KASLR offset removed (same as mainline kcov), so
+the runtime offset is derived from /proc/kallsyms and System.map / vmlinux
+(or a per-architecture default) and added back for symbolization; use
+--kaslr-offset to override. Records must contain at least one value word
+and one of the three record types, otherwise the parser resyncs word by
+word (e.g. after a userspace reset of area[0] mid-run).
+"""
+import os
+import sys
+import struct
+import ctypes
+import ctypes.util
+import argparse
+import fcntl
+import platform
+import subprocess
+import shutil
+
+# Constants -- must match include/uapi/linux/kcov_dataflow.h
+DF_TYPE_CMP = 0xC
+DF_TYPE_ENTRY = 0xE
+DF_TYPE_RET = 0xF
+MAGIC_BAD = 0xBADADD85
+BUF_SIZE = 1048576  # 1M words = 8MB
+
+# Record header word: bits 0-23 seq | 28-31 type | 32-47 nvals |
+# 48-55 arg/ret size | 56-63 arg index. Word 1 is the pc (KASLR offset
+# removed, like mainline kcov), word 2 the traced pointer (ENTRY/RET) or the
+# comparison type (CMP), then nvals value words.
+def hdr_seq(h):
+    return h & 0x00FFFFFF
+
+def hdr_type(h):
+    return (h >> 28) & 0xF
+
+def hdr_nvals(h):
+    return (h >> 32) & 0xFFFF
+
+def hdr_size(h):
+    return (h >> 48) & 0xFF
+
+def hdr_arg_idx(h):
+    return (h >> 56) & 0xFF
+
+RECORD_HDR_WORDS = 3
+
+# Runtime KASLR offset (see kaslr_offset()); added back to every recorded pc
+# so /proc/kallsyms lookups work, subtracted again for addr2line on vmlinux.
+KASLR_OFFSET = 0
+
+# Ioctl numbers
+def _IOR(t, nr, size):
+    return (2 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IOW(t, nr, size):
+    return (1 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IO(t, nr):
+    return (ord(t) << 8) | nr
+
+KCOV_DF_INIT_TRACK = _IOR('d', 1, 8)
+KCOV_DF_ENABLE = _IO('d', 100)
+KCOV_DF_DISABLE = _IO('d', 101)
+KCOV_DF_REMOTE_ENABLE = _IOW('d', 102, 8)  # arg: pointer to a __u64 handle
+KCOV_DF_REMOTE_DISABLE = _IO('d', 103)
+
+KCOV_DF_PATH = "/sys/kernel/debug/kcov_dataflow"
+
+# Every test module creates its trigger file(s) in this debugfs directory;
+# writing to them runs the instrumented test functions.
+TRIGGER_DIR = "/sys/kernel/debug/kcov_dataflow_test"
+
+# Remote handle registered with KCOV_DF_REMOTE_ENABLE; must match the
+# kcov_df_remote_start(1) call in the rust_kworker_remote test module
+# (KCOV_SUBSYSTEM_COMMON, instance 1).
+REMOTE_HANDLE = 1
+
+# syscall numbers
+_machine = platform.machine()
+if _machine == "aarch64":
+    SYS_FINIT_MODULE = 273
+    SYS_DELETE_MODULE = 106
+else:  # x86_64
+    SYS_FINIT_MODULE = 313
+    SYS_DELETE_MODULE = 176
+
+SELFTEST_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def load_kallsyms():
+    """Load kernel symbols for PC resolution."""
+    syms = []
+    try:
+        with open("/proc/kallsyms") as f:
+            for line in f:
+                parts = line.split()
+                if len(parts) >= 3:
+                    addr = int(parts[0], 16)
+                    name = parts[2]
+                    mod = parts[3].strip("[]") if len(parts) > 3 else ""
+                    syms.append((addr, name, mod))
+    except (PermissionError, FileNotFoundError):
+        pass
+    syms.sort()
+    return syms
+
+
+def runtime_text(syms):
+    """Runtime address of _text from kallsyms, 0 if hidden."""
+    return next((a for a, n, m in syms if n == "_text" and not m), 0)
+
+
+# Link-time address of _text per architecture, used only when neither
+# System.map nor vmlinux is available: x86_64 __START_KERNEL
+# (__START_KERNEL_map + CONFIG_PHYSICAL_START), arm64 KIMAGE_VADDR.
+LINKTIME_TEXT_DEFAULT = {
+    "x86_64": 0xffffffff81000000,
+    "aarch64": 0xffff800080000000,
+}
+
+
+def linktime_text(vmlinux=None):
+    """Return (link-time address of _text, source description) or (0, "")."""
+    rel = os.uname().release
+    candidates = []
+    if vmlinux:
+        candidates.append(os.path.join(os.path.dirname(vmlinux) or ".", "System.map"))
+    candidates += ["System.map", f"/boot/System.map-{rel}",
+                   f"/usr/lib/debug/boot/System.map-{rel}"]
+    for sm in candidates:
+        try:
+            with open(sm) as f:
+                for line in f:
+                    parts = line.split()
+                    if len(parts) == 3 and parts[2] == "_text":
+                        return int(parts[0], 16), sm
+        except (OSError, ValueError):
+            continue
+    if vmlinux and shutil.which("nm"):
+        try:
+            r = subprocess.run(["nm", "--defined-only", vmlinux],
+                               capture_output=True, text=True, timeout=300)
+            for line in r.stdout.splitlines():
+                parts = line.split()
+                if len(parts) == 3 and parts[2] == "_text":
+                    return int(parts[0], 16), f"nm {vmlinux}"
+        except (OSError, subprocess.TimeoutExpired):
+            pass
+    link = LINKTIME_TEXT_DEFAULT.get(platform.machine(), 0)
+    return link, f"{platform.machine()} default" if link else ""
+
+
+def kaslr_offset(syms, vmlinux=None):
+    """
+    Runtime KASLR offset: recorded PCs have it removed (kcov's
+    canonicalize_ip()), /proc/kallsyms has it applied. Computed as the
+    runtime _text (kallsyms) minus the link-time _text (System.map, nm
+    vmlinux, or the architecture default). KASLR offsets are 2 MiB aligned
+    on x86_64 and arm64, which is used as a sanity check on the result.
+    """
+    runtime = runtime_text(syms)
+    if not runtime:
+        print("# warning: _text not in /proc/kallsyms (kptr_restrict?); "
+              "PCs will not symbolize", file=sys.stderr)
+        return 0
+    link, source = linktime_text(vmlinux)
+    if not link:
+        print(f"# warning: no System.map/vmlinux and no default _text for "
+              f"{platform.machine()}; pass --kaslr-offset", file=sys.stderr)
+        return 0
+    off = runtime - link
+    if off % (2 << 20):
+        print(f"# warning: kaslr offset 0x{off:x} from {source} is not 2 MiB "
+              f"aligned; check CONFIG_PHYSICAL_START/KIMAGE_VADDR or pass "
+              f"--kaslr-offset", file=sys.stderr)
+    return off
+
+
+# Rust symbol demangling via llvm-cxxfilt or rustfilt
+_demangler = None
+
+def _init_demangler():
+    global _demangler
+    for tool in ["llvm-cxxfilt", "rustfilt", "c++filt"]:
+        path = shutil.which(tool)
+        if path:
+            _demangler = path
+            return
+    _demangler = ""
+
+_demangled = {}
+
+def demangle(name):
+    """Demangle a Rust/C++ symbol name (memoized: one process per name)."""
+    global _demangler
+    if _demangler is None:
+        _init_demangler()
+    if not _demangler or not name.startswith("_R"):
+        return name
+    if name not in _demangled:
+        try:
+            r = subprocess.run([_demangler, name], capture_output=True,
+                               text=True, timeout=2)
+            _demangled[name] = r.stdout.strip() if r.returncode == 0 else name
+        except (OSError, subprocess.TimeoutExpired):
+            _demangled[name] = name
+    return _demangled[name]
+
+
+def find_vmlinux(vmlinux=None):
+    """Locate vmlinux for addr2line: explicit path, else the usual places."""
+    if vmlinux:
+        return vmlinux
+    for p in ["vmlinux", "/boot/vmlinux", "/usr/lib/debug/boot/vmlinux"]:
+        if os.path.exists(p):
+            return p
+    return None
+
+
+def _a2l_target(pc, vmlinux, ko_path, mod_text_base):
+    """(binary, address in it) to symbolize pc with, or None."""
+    if ko_path and mod_text_base and pc >= mod_text_base:
+        return ko_path, pc - mod_text_base
+    if vmlinux:
+        return vmlinux, pc - KASLR_OFFSET  # vmlinux holds link-time addresses
+    return None
+
+
+def resolve_lines(pcs, vmlinux, cache, ko_path=None, mod_text_base=0):
+    """
+    Resolve every pc in @pcs to file:line into @cache, one addr2line run
+    per binary: a DWARF5 vmlinux takes hundreds of ms to open, so one
+    process per record does not scale to thousands of records.
+    """
+    todo = {}
+    for pc in pcs:
+        if pc in cache:
+            continue
+        cache[pc] = ""
+        tgt = _a2l_target(pc, vmlinux, ko_path, mod_text_base)
+        if tgt:
+            todo.setdefault(tgt[0], []).append((pc, tgt[1]))
+    for binary, pairs in todo.items():
+        try:
+            r = subprocess.run(
+                ["addr2line", "-e", binary] + [f"0x{a:x}" for _, a in pairs],
+                capture_output=True, text=True, timeout=300)
+        except (subprocess.TimeoutExpired, FileNotFoundError):
+            continue
+        for (pc, _), loc in zip(pairs, r.stdout.splitlines()):
+            loc = loc.strip()
+            if loc and loc != "??:0" and loc != "??:?":
+                # Shorten path: keep only filename:line
+                cache[pc] = loc.rsplit("/", 1)[-1]
+
+
+def resolve_line(pc, vmlinux, cache, ko_path=None, mod_text_base=0):
+    """Resolve one PC to source file:line using addr2line (cached)."""
+    if pc not in cache:
+        resolve_lines([pc], vmlinux, cache, ko_path, mod_text_base)
+    return cache[pc]
+
+
+def get_kernel_meta():
+    """Collect kernel build metadata."""
+    meta = {"release": os.uname().release}
+    try:
+        with open("/proc/version") as f:
+            v = f.read().strip()
+        meta["version"] = v
+        # Extract compiler version
+        if "gcc" in v.lower():
+            meta["compiler"] = v.split("(")[1].split(")")[0] if "(" in v else ""
+        elif "clang" in v.lower():
+            idx = v.lower().find("clang")
+            meta["compiler"] = v[idx:idx+30].split(")")[0]
+    except OSError:
+        pass
+    return meta
+
+
+def print_kernel_meta(meta, ko_path=None):
+    """Print kernel metadata header/footer."""
+    print(f"# {'=' * 60}")
+    print(f"# Kernel: {meta.get('release', 'unknown')}")
+    print(f"# Build:  {meta.get('version', 'unknown')[:80]}")
+    if meta.get('compiler'):
+        print(f"# Compiler: {meta['compiler']}")
+    # Read rustc version from .ko .comment section
+    if ko_path:
+        try:
+            r = subprocess.run(
+                ["readelf", "-p", ".comment", ko_path],
+                capture_output=True, text=True, timeout=5)
+            for line in r.stdout.splitlines():
+                if "rustc" in line:
+                    ver = line.split("]", 1)[-1].strip()
+                    print(f"# Rustc: {ver}")
+                    break
+        except (OSError, subprocess.TimeoutExpired):
+            pass
+    print(f"# {'=' * 60}")
+
+
+def lookup(pc, syms):
+    """Nearest kallsyms entry <= pc as (name, offset, module) or None."""
+    if not syms:
+        return None
+    lo, hi = 0, len(syms) - 1
+    while lo < hi:
+        mid = (lo + hi + 1) // 2
+        if syms[mid][0] <= pc:
+            lo = mid
+        else:
+            hi = mid - 1
+    addr, name, mod = syms[lo]
+    if addr > pc:
+        return None
+    return name, pc - addr, mod
+
+
+def symbolize(pc, syms):
+    """Find nearest symbol <= pc. Returns (display_name, module_tag)."""
+    hit = lookup(pc, syms)
+    if not hit:
+        return f"0x{pc:x}", ""
+    name, offset, mod = hit
+    dname = demangle(name)
+    display = f"{dname}+0x{offset:x}" if offset else dname
+    return display, f" [{mod}]" if mod else ""
+
+
+def format_val(v):
+    """Format a captured value."""
+    if v == MAGIC_BAD:
+        return "FAULT"
+    if v == 0:
+        return "0x0"
+    return f"0x{v:x}"
+
+
+def find_module(name):
+    """
+    Find the .ko for test @name: <name>/<name>.ko in the source tree, or
+    <name>.ko next to this script in an installed (make install) tree.
+    """
+    for ko_path in (os.path.join(SELFTEST_DIR, name, f"{name}.ko"),
+                    os.path.join(SELFTEST_DIR, f"{name}.ko")):
+        if os.path.exists(ko_path):
+            return ko_path
+    return None
+
+
+def finit_module(ko_path):
+    """Load a kernel module via finit_module syscall."""
+    libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+    fd = os.open(ko_path, os.O_RDONLY)
+    ret = libc.syscall(SYS_FINIT_MODULE, fd, b"", 0)
+    os.close(fd)
+    if ret != 0:
+        errno = ctypes.get_errno()
+        raise OSError(errno, f"finit_module({ko_path}): {os.strerror(errno)}")
+
+
+def delete_module(name):
+    """Unload a kernel module."""
+    libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+    ret = libc.syscall(SYS_DELETE_MODULE, name.encode(), 0)
+    if ret != 0:
+        errno = ctypes.get_errno()
+        raise OSError(errno, f"delete_module({name}): {os.strerror(errno)}")
+
+
+def trigger_module():
+    """
+    Write to every trigger file the loaded module created under TRIGGER_DIR.
+    Opened without O_CREAT: debugfs directories have no ->create, so a
+    "w"-mode open of a missing name fails with EOPNOTSUPP, not ENOENT.
+    """
+    try:
+        names = sorted(os.listdir(TRIGGER_DIR))
+    except OSError:
+        names = []
+    hits = []
+    for n in names:
+        path = os.path.join(TRIGGER_DIR, n)
+        try:
+            fd = os.open(path, os.O_WRONLY)
+        except OSError:
+            continue
+        try:
+            os.write(fd, b"1")
+        finally:
+            os.close(fd)
+        hits.append(path)
+    if not hits:
+        raise FileNotFoundError(f"no trigger file under {TRIGGER_DIR}")
+    return hits
+
+
+def parse_records(buf, total_words):
+    """Parse the ring buffer into a list of records."""
+    records = []
+    pos = 1
+    end = min(1 + total_words, BUF_SIZE)
+    while pos + RECORD_HDR_WORDS <= end:
+        hdr = buf[pos]
+        rtype = hdr_type(hdr)
+        num_vals = hdr_nvals(hdr)
+
+        # Every record the kernel writes has nvals >= 1 and a known type;
+        # anything else is garbage (e.g. a userspace reset mid-run): resync.
+        if rtype not in (DF_TYPE_ENTRY, DF_TYPE_RET, DF_TYPE_CMP) \
+                or num_vals == 0 or pos + RECORD_HDR_WORDS + num_vals > end:
+            pos += 1
+            continue
+
+        pc = int(buf[pos + 1]) + KASLR_OFFSET
+        ptr = int(buf[pos + 2])  # ENTRY/RET: traced pointer; CMP: cmp type
+        if rtype == DF_TYPE_CMP:
+            pos += RECORD_HDR_WORDS + num_vals
+            continue
+
+        # Valid records always have a non-zero PC (kernel text address)
+        if pc == 0:
+            pos += 1
+            continue
+
+        vals = [int(buf[pos + RECORD_HDR_WORDS + vi]) for vi in range(num_vals)]
+        records.append({
+            "type": rtype,
+            "seq": hdr_seq(hdr),
+            "pc": pc,
+            "ptr": ptr,
+            "arg_idx": hdr_arg_idx(hdr),
+            "size": hdr_size(hdr),
+            "val": vals[0],
+            "vals": vals,
+        })
+        pos += RECORD_HDR_WORDS + num_vals
+    return records
+
+
+class Capture:
+    """Everything run_capture() collected for one module run."""
+
+    def __init__(self, ko_path, mod_name, records, syms, total_words,
+                 mod_text_start, kaslr_off):
+        self.ko_path = ko_path
+        self.mod_name = mod_name
+        self.records = records
+        self.syms = syms
+        self.total_words = total_words
+        self.mod_text_start = mod_text_start
+        self.kaslr_offset = kaslr_off
+        self.runtime_text = runtime_text(syms)
+        self._mod_syms = any(m == mod_name for _, _, m in syms)
+        # Aliases: rustc's merge-functions makes identical bodies (e.g. the
+        # one-field rsf_1 and rstf_1) share one address, so a PC can carry
+        # several names.
+        self._names = {}
+        for addr, name, mod in syms:
+            self._names.setdefault((addr, mod), set()).add(name)
+
+    def is_module_pc(self, pc):
+        """True if pc lies in the test module (kallsyms, else .text start)."""
+        if self._mod_syms:
+            hit = lookup(pc, self.syms)
+            return bool(hit) and hit[2] == self.mod_name
+        # Fallback: if no module symbols (kptr_restrict), use .text start
+        return bool(self.mod_text_start) and pc >= self.mod_text_start
+
+    def funcs(self, rec):
+        """All raw kallsyms names of the function a record belongs to."""
+        hit = lookup(rec["pc"], self.syms)
+        if not hit:
+            return set()
+        name, offset, mod = hit
+        return self._names.get((rec["pc"] - offset, mod), {name})
+
+    def module_records(self):
+        return [r for r in self.records if self.is_module_pc(r["pc"])]
+
+    def context_records(self, n):
+        """Module records plus n records before/after each of them."""
+        keep = set()
+        for i, r in enumerate(self.records):
+            if self.is_module_pc(r["pc"]):
+                keep.update(range(max(0, i - n),
+                                  min(len(self.records), i + n + 1)))
+        return [self.records[i] for i in sorted(keep)]
+
+
+def run_capture(ko_path, remote=False, vmlinux=None, kaslr_override=None,
+                log=None):
+    """
+    Load @ko_path, record while its trigger file(s) are written, unload it
+    and return a Capture. @remote publishes the buffer for REMOTE_HANDLE
+    instead of enabling recording for this task. Raises OSError.
+    """
+    global KASLR_OFFSET
+    log = log or (lambda msg: print(f"# {msg}"))
+
+    # Ensure kallsyms shows real addresses
+    try:
+        with open("/proc/sys/kernel/kptr_restrict", "w") as f:
+            f.write("0")
+    except OSError:
+        pass
+
+    df_fd = os.open(KCOV_DF_PATH, os.O_RDWR)
+    try:
+        # Init + mmap
+        fcntl.ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)
+        libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+        libc.mmap.restype = ctypes.c_void_p
+        libc.mmap.argtypes = [
+            ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
+            ctypes.c_int, ctypes.c_int, ctypes.c_long
+        ]
+        buf_ptr = libc.mmap(None, BUF_SIZE * 8, 0x3, 0x01, df_fd, 0)
+        if buf_ptr == ctypes.c_void_p(-1).value:
+            errno = ctypes.get_errno()
+            raise OSError(errno, f"mmap: {os.strerror(errno)}")
+        buf = (ctypes.c_uint64 * BUF_SIZE).from_address(buf_ptr)
+
+        # Load module first (its init generates noise with INSTRUMENT_ALL)
+        mod_name = os.path.basename(ko_path).replace(".ko", "")
+        finit_module(ko_path)
+        log(f"Loaded {mod_name}")
+        try:
+            # Module .text address, the PC filter fallback without kallsyms
+            mod_text_start = 0
+            try:
+                with open(f"/sys/module/{mod_name}/sections/.text") as f:
+                    mod_text_start = int(f.read().strip(), 16)
+            except (OSError, ValueError):
+                pass
+
+            # Enable recording AFTER load, BEFORE trigger (no loader noise).
+            # Remote: the handle is passed by pointer (a __u64 in a buffer),
+            # so the full 64-bit value survives 32-bit/compat callers.
+            if remote:
+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_ENABLE,
+                            struct.pack("Q", REMOTE_HANDLE))
+            else:
+                fcntl.ioctl(df_fd, KCOV_DF_ENABLE, 0)
+            buf[0] = 0
+            try:
+                for path in trigger_module():
+                    log(f"Triggered {path}")
+            finally:
+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_DISABLE if remote
+                            else KCOV_DF_DISABLE, 0)
+
+            # Read kallsyms while the module is still loaded
+            syms = load_kallsyms()
+        finally:
+            try:
+                delete_module(mod_name)
+            except OSError as e:
+                log(f"warning: {e}")
+
+        if kaslr_override is not None:
+            KASLR_OFFSET = kaslr_override
+        else:
+            KASLR_OFFSET = kaslr_offset(syms, find_vmlinux(vmlinux))
+
+        total = int(buf[0])
+        records = parse_records(buf, total)
+        return Capture(ko_path, mod_name, records, syms, total,
+                       mod_text_start, KASLR_OFFSET)
+    finally:
+        os.close(df_fd)
+
+
+def print_raw(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+    """Print records in raw format with source line on left."""
+    if cache is None:
+        cache = {}
+    # Pre-resolve all locations (one addr2line run) to find max width
+    resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+                  mod_text_base)
+    locs = [cache[r["pc"]] for r in records]
+    max_w = max((len(l) for l in locs if l), default=0)
+    max_w = max(max_w, 10)  # minimum width
+
+    for i, r in enumerate(records):
+        name, mod = symbolize(r["pc"], syms)
+        sym = f"{name}{mod}"
+        t = "ENTRY" if r["type"] == DF_TYPE_ENTRY else "RET  "
+        arg_idx = r["arg_idx"]
+        size = r["size"]
+        left = f"{locs[i]:>{max_w}s}" if locs[i] else f"{'':>{max_w}s}"
+        vals = format_val(r["val"]) if len(r["vals"]) == 1 else \
+            "{" + ", ".join(format_val(v) for v in r["vals"]) + "}"
+        print(f"{left}   [{t}] seq={r['seq']:3d} {sym} "
+              f"arg[{arg_idx}]({size}) @0x{r['ptr']:x} = {vals}")
+
+
+def print_tree(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+    """Print records as indented call tree with source line on left."""
+    if cache is None:
+        cache = {}
+    # Pre-resolve all PCs (one addr2line run) for alignment
+    resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+                  mod_text_base)
+    max_w = max((len(v) for v in cache.values() if v), default=10)
+    max_w = max(max_w, 10)
+
+    depth = 0
+    call_stack = []  # Stack of (name, mod, args_str, pc) for matching returns
+    i = 0
+    while i < len(records):
+        r = records[i]
+        name, mod = symbolize(r["pc"], syms)
+
+        if r["type"] == DF_TYPE_ENTRY:
+            # Collect all args for this call (same PC, consecutive entries);
+            # order by index, as the pass emits dead-arg traces last.
+            args = []
+            pc = r["pc"]
+            while i < len(records) and records[i]["type"] == DF_TYPE_ENTRY \
+                    and records[i]["pc"] == pc:
+                vals = records[i]["vals"]
+                if len(vals) > 1:
+                    fields = ", ".join(format_val(v) for v in vals)
+                    args.append((records[i]["arg_idx"], "{" + fields + "}"))
+                else:
+                    args.append((records[i]["arg_idx"],
+                                 format_val(records[i]["val"])))
+                i += 1
+            args_str = ", ".join(a for _, a in sorted(args, key=lambda x: x[0]))
+            call_stack.append((name, mod, args_str, pc))
+            depth += 1
+        else:
+            # Pop void calls (no return record) until we find matching PC
+            while call_stack and call_stack[-1][3] != r["pc"]:
+                depth = max(0, depth - 1)
+                indent = "  " * depth
+                vname, vmod, vargs, vpc = call_stack.pop()
+                loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+                left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+                print(f"{left}   {indent}{vname}({vargs}){vmod}")
+            depth = max(0, depth - 1)
+            indent = "  " * depth
+            ret_size = r["size"]
+            loc = resolve_line(r["pc"], vmlinux, cache, ko_path, mod_text_base)
+            left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+            if call_stack:
+                cname, cmod, cargs, _ = call_stack.pop()
+                if ret_size == 0:
+                    print(f"{left}   {indent}{cname}({cargs}){cmod}")
+                else:
+                    print(f"{left}   {indent}{format_val(r['val'])} = {cname}({cargs}){cmod}")
+            else:
+                if ret_size == 0:
+                    print(f"{left}   {indent}{name}(){mod}")
+                else:
+                    print(f"{left}   {indent}{format_val(r['val'])} = {name}(){mod}")
+            i += 1
+
+    # Flush remaining void calls on the stack
+    while call_stack:
+        depth = max(0, depth - 1)
+        indent = "  " * depth
+        vname, vmod, vargs, vpc = call_stack.pop()
+        loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+        left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+        print(f"{left}   {indent}{vname}({vargs}){vmod}")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Load a test module with kcov_dataflow and view records")
+    parser.add_argument("module", help="Test module name (e.g. eight_struct_args_c)")
+    parser.add_argument("--raw", action="store_true",
+                        help="Print raw records instead of tree")
+    parser.add_argument("--ko", help="Explicit path to .ko file")
+    parser.add_argument("--context", "-C", type=int, default=0,
+                        help="Show N records before/after each module record")
+    parser.add_argument("--vmlinux", help="Path to vmlinux for addr2line")
+    parser.add_argument("--remote", action="store_true",
+                        help="Use KCOV_DF_REMOTE_ENABLE for kworker capture")
+    parser.add_argument("--kaslr-offset", type=lambda x: int(x, 0),
+                        help="Override the runtime KASLR offset added to PCs")
+    args = parser.parse_args()
+
+    ko_path = args.ko or find_module(args.module)
+    if not ko_path or not os.path.exists(ko_path):
+        print(f"Cannot find module for '{args.module}'", file=sys.stderr)
+        print("Build it first: make -C tools/testing/selftests "
+              "TARGETS=kcov_dataflow LLVM=1 CC=clang", file=sys.stderr)
+        sys.exit(1)
+
+    try:
+        cap = run_capture(ko_path, remote=args.remote, vmlinux=args.vmlinux,
+                          kaslr_override=args.kaslr_offset)
+    except OSError as e:
+        print(f"{args.module}: {e}", file=sys.stderr)
+        sys.exit(1)
+
+    print(f"# Captured {cap.total_words} words (kaslr_offset=0x{cap.kaslr_offset:x}, "
+          f"_text=0x{cap.runtime_text:x})")
+    print(f"# {len(cap.records)} records")
+
+    if cap.syms or cap.mod_text_start:
+        if args.context > 0:
+            records = cap.context_records(args.context)
+            print(f"# showing {len(records)} records with context={args.context} "
+                  f"around {cap.mod_name}\n")
+        else:
+            records = cap.module_records()
+            print(f"# {len(records)} from {cap.mod_name}\n")
+    else:
+        records = cap.records
+        print("")
+
+    meta = get_kernel_meta()
+    print_kernel_meta(meta, ko_path=ko_path)
+
+    vmlinux = find_vmlinux(args.vmlinux)
+    show = print_raw if args.raw else print_tree
+    show(records, cap.syms, vmlinux, {}, ko_path, cap.mod_text_start)
+
+    print_kernel_meta(meta, ko_path=ko_path)
+
+
+if __name__ == "__main__":
+    main()
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 0000000000000..1cb3d9b41c070
--- /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 0000000000000..55072de189d31
--- /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 0000000000000..d7b04c368ced9
--- /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

3/2 2026/09/03 13:17 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 75933
							output: 19
							thoughts: 930
						

Thoughts:
**KCOV Dataflow Coverage: Initial Assessment**

Okay, so this patch is introducing KCOV dataflow coverage. My initial reaction is, "Intriguing, let's see what's what." Looks like the core is in `kernel/kcov_dataflow.c`, with supporting files and modifications across the kernel – things like `kcov.c`, `exit.c`, `fork.c`, `Makefile`, and the associated header files are all touched. We've got Kconfig options (`CONFIG_KCOV_DATAFLOW_*`) in `lib/Kconfig.debug`, a debugfs device at `/sys/kernel/debug/kcov_dataflow`, and a few ioctls to control the whole thing. The usual file operations are also present: `mmap`, `open`, `release`, `shrinker`, etc.

My immediate thought is: "Is this going to be easily reachable? Is it fuzzable?"  Before I dive deep, I need to check the build prerequisites and dependencies. Let's see... the Kconfig options have some dependencies, which is expected. I see `depends on KCOV`, `depends on CC_IS_CLANG`, `depends on DEBUG_INFO`, and the killer – it `depends on $(cc-option,-fsanitize-coverage=trace-args)`.  And wait a second, there's another one: `depends on $(cc-option,-fsanitize-coverage=trace-ret)`.

Ah, here's the rub!  I recall from experience that the standard compilers don't typically expose these `-fsanitize-coverage` options.  The documentation confirms it – standard compilers won't enable these Kconfig options. This is a crucial detail. I need to know if this is going to be standard environment reachable.

Next, I'll need to confirm that `kcov_dataflow.o` is actually built into the kernel. Looking at `kernel/Makefile`, it conditionally includes `kcov_dataflow.o` if either `CONFIG_KCOV_DATAFLOW_ARGS` or `CONFIG_KCOV_DATAFLOW_RET` are set.  Given that we've established the clang and sanitizer-coverage dependency, this seems like it'll require a specific compiler setup.

Now, let's think about the attack surface. From the code, anyone with access to debugfs can open `/sys/kernel/debug/kcov_dataflow` as a character device. The ioctls seem interesting: `KCOV_DF_INIT_TRACK`, `KCOV_DF_ENABLE`, `KCOV_DF_DISABLE`, `KCOV_DF_REMOTE_ENABLE`, and `KCOV_DF_REMOTE_DISABLE`. And the standard `mmap`, `fork`, `exit` functionality. If the fuzzer runs as root, or with debugfs mounted, it can open the device and trigger the ioctls and everything else.

From my initial assessment, the code itself *is* reachable code, but it's behind a specific compiler setup. The code itself looks like a standard kernel character device implementation, with ioctls, memory management, refcounting, locking, task hooks, and all that. It's a new interface, so it could easily have new vulnerabilities. Now I need to check the other modified files. I'll need `grepper` or just analyze the diff more closely. I need to understand all the changes, especially in `kernel/kcov_dataflow.c` and how it interacts with the rest of the kernel.



4/2 2026/09/03 13:17 tool grepper
0m Args:
{
  "Expression": "kcov_dataflow"
}

Results:
{
  "Output": "\nFull output is too long, showing 500 out of 561 lines.\nUse more precise expression if possible.\n\nDocumentation/dev-tools/kcov-dataflow.rst=63=return value data for a single syscall:\n--\nDocumentation/dev-tools/kcov-dataflow.rst-75-\nDocumentation/dev-tools/kcov-dataflow.rst:76:    #include \u003clinux/kcov_dataflow.h\u003e   /* ioctls, record layout, helpers */\nDocumentation/dev-tools/kcov-dataflow.rst-77-    #define BUF_SIZE            (1 \u003c\u003c 20)  /* 1M words = 8MB */\n--\nDocumentation/dev-tools/kcov-dataflow.rst-83-\nDocumentation/dev-tools/kcov-dataflow.rst:84:        fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\nDocumentation/dev-tools/kcov-dataflow.rst-85-        if (fd == -1)\n--\nDocumentation/dev-tools/kcov-dataflow.rst=181=has been written, so a consumer that walks ``area[0]`` words never leaves\nDocumentation/dev-tools/kcov-dataflow.rst:182:its mapping. All of the above is defined in ``include/uapi/linux/kcov_dataflow.h``\nDocumentation/dev-tools/kcov-dataflow.rst-183-(``KCOV_DF_HDR_*()``, ``KCOV_DF_RECORD_WORDS()``).\n--\nDocumentation/dev-tools/kcov-dataflow.rst=229=KCOV-Dataflow is completely independent from legacy KCOV:\nDocumentation/dev-tools/kcov-dataflow.rst-230-\nDocumentation/dev-tools/kcov-dataflow.rst:231:- Separate device: ``/sys/kernel/debug/kcov_dataflow``\nDocumentation/dev-tools/kcov-dataflow.rst-232-- Separate ioctl namespace (``'d'`` vs ``'c'``)\n--\nDocumentation/dev-tools/kcov-dataflow.rst=258=Automated tests and visualization tools are in\nDocumentation/dev-tools/kcov-dataflow.rst:259:``tools/testing/selftests/kcov_dataflow/``::\nDocumentation/dev-tools/kcov-dataflow.rst-260-\nDocumentation/dev-tools/kcov-dataflow.rst-261-        # Automated ioctl interface test (TAP output):\nDocumentation/dev-tools/kcov-dataflow.rst:262:        make -C tools/testing/selftests/kcov_dataflow\nDocumentation/dev-tools/kcov-dataflow.rst-263-        vng --user root --exec \\\nDocumentation/dev-tools/kcov-dataflow.rst:264:          tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\nDocumentation/dev-tools/kcov-dataflow.rst-265-\nDocumentation/dev-tools/kcov-dataflow.rst-266-        # Load a test module and view captured records:\nDocumentation/dev-tools/kcov-dataflow.rst:267:        make LLVM=1 CC=clang M=tools/testing/selftests/kcov_dataflow/eight_struct_args_c modules\nDocumentation/dev-tools/kcov-dataflow.rst-268-        vng --user root --exec \\\nDocumentation/dev-tools/kcov-dataflow.rst:269:          \"python3 tools/testing/selftests/kcov_dataflow/trigger-view.py \\\nDocumentation/dev-tools/kcov-dataflow.rst-270-            eight_struct_args_c --ko \\\nDocumentation/dev-tools/kcov-dataflow.rst:271:            tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.ko\"\nDocumentation/dev-tools/kcov-dataflow.rst-272-\nDocumentation/dev-tools/kcov-dataflow.rst-273-        # Binderfs ioctl capture test (requires CONFIG_ANDROID_BINDER_IPC):\nDocumentation/dev-tools/kcov-dataflow.rst:274:        make -C tools/testing/selftests/kcov_dataflow/binderfs\nDocumentation/dev-tools/kcov-dataflow.rst-275-        vng --user root --exec \\\nDocumentation/dev-tools/kcov-dataflow.rst:276:          tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\nDocumentation/dev-tools/kcov-dataflow.rst-277-\nDocumentation/dev-tools/kcov-dataflow.rst:278:See ``tools/testing/selftests/kcov_dataflow/README.rst`` for details.\nDocumentation/dev-tools/kcov-dataflow.rst-279-\n--\nDocumentation/dev-tools/kcov-dataflow.rst=287=buffer atomically.\n--\nDocumentation/dev-tools/kcov-dataflow.rst-299-\nDocumentation/dev-tools/kcov-dataflow.rst:300:    #include \u003clinux/kcov_dataflow.h\u003e   /* ioctls, record layout, helpers */\nDocumentation/dev-tools/kcov-dataflow.rst-301-    #define BUF_SIZE            (1 \u003c\u003c 20)\n--\nDocumentation/dev-tools/kcov-dataflow.rst-304-    {\nDocumentation/dev-tools/kcov-dataflow.rst:305:        int fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\nDocumentation/dev-tools/kcov-dataflow.rst-306-        ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);\n--\nDocumentation/dev-tools/kcov-dataflow.rst=338=associated with a descriptor at a time. For true multi-process tracing,\nDocumentation/dev-tools/kcov-dataflow.rst:339:open a separate ``kcov_dataflow`` fd per child, or disable in the parent\nDocumentation/dev-tools/kcov-dataflow.rst-340-before the child enables (as shown above -- the parent is blocked in\n--\nDocumentation/dev-tools/kcov-dataflow.rst=354=User space setup:\n--\nDocumentation/dev-tools/kcov-dataflow.rst-365-    #include \u003clinux/kcov.h\u003e            /* kcov_remote_handle() */\nDocumentation/dev-tools/kcov-dataflow.rst:366:    #include \u003clinux/kcov_dataflow.h\u003e\nDocumentation/dev-tools/kcov-dataflow.rst-367-    #define BUF_SIZE                (1 \u003c\u003c 20)\n--\nDocumentation/dev-tools/kcov-dataflow.rst-370-    {\nDocumentation/dev-tools/kcov-dataflow.rst:371:        int fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\nDocumentation/dev-tools/kcov-dataflow.rst-372-        ioctl(fd, KCOV_DF_INIT_TRACK, BUF_SIZE);\n--\nDocumentation/userspace-api/ioctl/ioctl-number.rst=73=Code  Seq#    Include File                                             Comments\n--\nDocumentation/userspace-api/ioctl/ioctl-number.rst-242-'d'   F0-FF  linux/digi1.h\nDocumentation/userspace-api/ioctl/ioctl-number.rst:243:'d'   01     uapi/linux/kcov_dataflow.h                                conflict!\nDocumentation/userspace-api/ioctl/ioctl-number.rst:244:'d'   64-67  uapi/linux/kcov_dataflow.h                                conflict!\nDocumentation/userspace-api/ioctl/ioctl-number.rst-245-'e'   all    linux/digi1.h                                             conflict!\n--\nMAINTAINERS=14079=F:\tinclude/uapi/linux/kcov.h\nMAINTAINERS:14080:F:\tinclude/uapi/linux/kcov_dataflow.h\nMAINTAINERS-14081-F:\tkernel/kcov.c\nMAINTAINERS:14082:F:\tkernel/kcov_dataflow.c\nMAINTAINERS-14083-F:\tscripts/Makefile.kcov\n--\ninclude/linux/kcov.h=30=void kcov_task_exit(struct task_struct *t);\n--\ninclude/linux/kcov.h-32-#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)\ninclude/linux/kcov.h:33:void kcov_dataflow_task_init(struct task_struct *t);\ninclude/linux/kcov.h:34:void kcov_dataflow_task_exit(struct task_struct *t);\ninclude/linux/kcov.h-35-#else\ninclude/linux/kcov.h:36:static inline void kcov_dataflow_task_init(struct task_struct *t) {}\ninclude/linux/kcov.h:37:static inline void kcov_dataflow_task_exit(struct task_struct *t) {}\ninclude/linux/kcov.h-38-#endif\n--\ninclude/linux/kcov.h=139=static inline void kcov_remote_stop_softirq(void) {}\n--\ninclude/linux/kcov.h-143-/*\ninclude/linux/kcov.h:144: * kcov_dataflow remote API. The collector is a separate object from mainline\ninclude/linux/kcov.h-145- * kcov and is only linked in when at least one of the two capture modes is\ninclude/linux/kcov.h-146- * configured (see kernel/Makefile), so gate the declarations the same way\ninclude/linux/kcov.h:147: * kcov_dataflow_task_init() above is gated; a caller that brackets a region for\ninclude/linux/kcov.h-148- * both collectors then still builds on a KCOV-only config.\n--\ninclude/linux/kcov.h=184=static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)\n--\ninclude/linux/kcov.h-204- * top of mainline write_comp_data() (kcov_df_cmp_key is inc'd on dataflow enable\ninclude/linux/kcov.h:205: * in kcov_dataflow.c).\ninclude/linux/kcov.h-206- */\n--\ninclude/linux/sched.h=835=struct task_struct {\n--\ninclude/linux/sched.h-1568-\t/*\ninclude/linux/sched.h:1569:\t * The kcov_dataflow object this task's session belongs to, NULL when\ninclude/linux/sched.h-1570-\t * no session is active. The task holds a reference on it for the whole\n--\ninclude/linux/sched.h-1575-\t */\ninclude/linux/sched.h:1576:\tstruct kcov_dataflow\t\t*kcov_df;\ninclude/linux/sched.h-1577-\n--\ninclude/uapi/linux/kcov_dataflow.h-8-/*\ninclude/uapi/linux/kcov_dataflow.h:9: * User space ABI of /sys/kernel/debug/kcov_dataflow, see\ninclude/uapi/linux/kcov_dataflow.h-10- * Documentation/dev-tools/kcov-dataflow.rst.\n--\nkernel/Makefile=45=KMSAN_SANITIZE_kcov.o := n\nkernel/Makefile-46-\nkernel/Makefile:47:KCOV_INSTRUMENT_kcov_dataflow.o := n\nkernel/Makefile:48:KASAN_SANITIZE_kcov_dataflow.o := n\nkernel/Makefile:49:KCSAN_SANITIZE_kcov_dataflow.o := n\nkernel/Makefile:50:UBSAN_SANITIZE_kcov_dataflow.o := n\nkernel/Makefile:51:KMSAN_SANITIZE_kcov_dataflow.o := n\nkernel/Makefile-52-\n--\nkernel/Makefile=107=ifneq ($(CONFIG_KCOV_DATAFLOW_ARGS)$(CONFIG_KCOV_DATAFLOW_RET),)\nkernel/Makefile:108:obj-y += kcov_dataflow.o\nkernel/Makefile-109-endif\n--\nkernel/exit.c=928=void __noreturn do_exit(long code)\n--\nkernel/exit.c-941-\tkcov_task_exit(tsk);\nkernel/exit.c:942:\tkcov_dataflow_task_exit(tsk);\nkernel/exit.c-943-\tkmsan_task_exit(tsk);\n--\nkernel/fork.c=915=static struct task_struct *dup_task_struct(struct task_struct *orig, int node)\n--\nkernel/fork.c-987-\tkcov_task_init(tsk);\nkernel/fork.c:988:\tkcov_dataflow_task_init(tsk);\nkernel/fork.c-989-\tkmsan_task_create(tsk);\n--\nkernel/kcov_dataflow.c-4- *\nkernel/kcov_dataflow.c:5: * Exposes /sys/kernel/debug/kcov_dataflow, completely independent from\nkernel/kcov_dataflow.c-6- * /sys/kernel/debug/kcov. Own buffer, own ioctl, own mmap.\n--\nkernel/kcov_dataflow.c-10- * ioctls, the record layout and the header bit fields, is defined in\nkernel/kcov_dataflow.c:11: * \u003cuapi/linux/kcov_dataflow.h\u003e. In short, every record is\nkernel/kcov_dataflow.c-12- *\n--\nkernel/kcov_dataflow.c-16- */\nkernel/kcov_dataflow.c:17:#define pr_fmt(fmt) \"kcov_dataflow: \" fmt\nkernel/kcov_dataflow.c-18-\n--\nkernel/kcov_dataflow.c-42-#include \u003clinux/kcov.h\u003e\nkernel/kcov_dataflow.c:43:#include \u003cuapi/linux/kcov_dataflow.h\u003e\nkernel/kcov_dataflow.c-44-#include \u003casm/setup.h\u003e\n--\nkernel/kcov_dataflow.c-74-\nkernel/kcov_dataflow.c:75:struct kcov_dataflow {\nkernel/kcov_dataflow.c-76-\tstruct mutex\tlock;\n--\nkernel/kcov_dataflow.c=132=EXPORT_SYMBOL(kcov_df_cmp_key);\nkernel/kcov_dataflow.c-133-\nkernel/kcov_dataflow.c:134:static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which)\nkernel/kcov_dataflow.c-135-{\n--\nkernel/kcov_dataflow.c-145-\nkernel/kcov_dataflow.c:146:static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which)\nkernel/kcov_dataflow.c-147-{\n--\nkernel/kcov_dataflow.c-157-\nkernel/kcov_dataflow.c:158:static bool kcov_df_cmp_key_held(struct kcov_dataflow *df)\nkernel/kcov_dataflow.c-159-{\n--\nkernel/kcov_dataflow.c-162-#else\nkernel/kcov_dataflow.c:163:static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which) {}\nkernel/kcov_dataflow.c:164:static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which) {}\nkernel/kcov_dataflow.c:165:static bool kcov_df_cmp_key_held(struct kcov_dataflow *df) { return false; }\nkernel/kcov_dataflow.c-166-#endif\n--\nkernel/kcov_dataflow.c=172=struct kcov_df_remote {\nkernel/kcov_dataflow.c-173-\tu64\t\t\thandle;\nkernel/kcov_dataflow.c:174:\tstruct kcov_dataflow\t*df;\nkernel/kcov_dataflow.c-175-\tstruct hlist_node\thnode;\n--\nkernel/kcov_dataflow.c=178=static struct kcov_df_remote *kcov_df_remote_find(u64 handle)\n--\nkernel/kcov_dataflow.c-189-/* Unpublish @df's remote handle, if any; no new remote session can start. */\nkernel/kcov_dataflow.c:190:static void kcov_df_remote_unpublish(struct kcov_dataflow *df)\nkernel/kcov_dataflow.c-191-{\n--\nkernel/kcov_dataflow.c-205-\nkernel/kcov_dataflow.c:206:static void kcov_df_get(struct kcov_dataflow *df)\nkernel/kcov_dataflow.c-207-{\n--\nkernel/kcov_dataflow.c-217- */\nkernel/kcov_dataflow.c:218:static void kcov_df_put(struct kcov_dataflow *df)\nkernel/kcov_dataflow.c-219-{\n--\nkernel/kcov_dataflow.c=269=static void kcov_df_scratch_put(void *area)\n--\nkernel/kcov_dataflow.c-295- */\nkernel/kcov_dataflow.c:296:static void kcov_df_merge(struct kcov_dataflow *df, const u64 *scratch)\nkernel/kcov_dataflow.c-297-{\n--\nkernel/kcov_dataflow.c=682=EXPORT_SYMBOL(kcov_df_trace_cmp);\n--\nkernel/kcov_dataflow.c-685-/* Called from kernel/fork.c to clear inherited state. */\nkernel/kcov_dataflow.c:686:void kcov_dataflow_task_init(struct task_struct *t)\nkernel/kcov_dataflow.c-687-{\n--\nkernel/kcov_dataflow.c-696-/* Called from kernel/exit.c to tear down the exiting task's session, if any. */\nkernel/kcov_dataflow.c:697:void kcov_dataflow_task_exit(struct task_struct *t)\nkernel/kcov_dataflow.c-698-{\nkernel/kcov_dataflow.c:699:\tstruct kcov_dataflow *df = t-\u003ekcov_df;\nkernel/kcov_dataflow.c-700-\n--\nkernel/kcov_dataflow.c-742-\nkernel/kcov_dataflow.c:743:/* File operations for /sys/kernel/debug/kcov_dataflow */\nkernel/kcov_dataflow.c-744-\nkernel/kcov_dataflow.c=745=static int kcov_df_open(struct inode *inode, struct file *filep)\nkernel/kcov_dataflow.c-746-{\nkernel/kcov_dataflow.c:747:\tstruct kcov_dataflow *df;\nkernel/kcov_dataflow.c-748-\nkernel/kcov_dataflow.c:749:\tdf = kzalloc_obj(struct kcov_dataflow, GFP_KERNEL);\nkernel/kcov_dataflow.c-750-\tif (!df)\n--\nkernel/kcov_dataflow.c-761- */\nkernel/kcov_dataflow.c:762:static void kcov_df_disable_local(struct kcov_dataflow *df)\nkernel/kcov_dataflow.c-763-{\n--\nkernel/kcov_dataflow.c=775=static int kcov_df_close(struct inode *inode, struct file *filep)\nkernel/kcov_dataflow.c-776-{\nkernel/kcov_dataflow.c:777:\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\nkernel/kcov_dataflow.c-778-\tbool put_session = false;\n--\nkernel/kcov_dataflow.c=809=static int kcov_df_mmap(struct file *filep, struct vm_area_struct *vma)\nkernel/kcov_dataflow.c-810-{\nkernel/kcov_dataflow.c:811:\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\nkernel/kcov_dataflow.c-812-\tunsigned long size, off;\n--\nkernel/kcov_dataflow.c=840=static long kcov_df_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)\nkernel/kcov_dataflow.c-841-{\nkernel/kcov_dataflow.c:842:\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\nkernel/kcov_dataflow.c-843-\tbool put_session = false;\n--\nkernel/kcov_dataflow.c=985=void kcov_df_remote_start(u64 handle)\n--\nkernel/kcov_dataflow.c-987-\tstruct kcov_df_remote *remote;\nkernel/kcov_dataflow.c:988:\tstruct kcov_dataflow *df;\nkernel/kcov_dataflow.c-989-\tvoid *scratch;\n--\nkernel/kcov_dataflow.c=1062=void kcov_df_remote_stop(void)\nkernel/kcov_dataflow.c-1063-{\nkernel/kcov_dataflow.c:1064:\tstruct kcov_dataflow *df = current-\u003ekcov_df;\nkernel/kcov_dataflow.c-1065-\tvoid *scratch;\n--\nkernel/kcov_dataflow.c=1149=kcov_df_scratch_shrink_scan(struct shrinker *sh, struct shrink_control *sc)\n--\nkernel/kcov_dataflow.c-1174-\nkernel/kcov_dataflow.c:1175:static int __init kcov_dataflow_init(void)\nkernel/kcov_dataflow.c-1176-{\n--\nkernel/kcov_dataflow.c-1178-\nkernel/kcov_dataflow.c:1179:\tdebugfs_create_file_unsafe(\"kcov_dataflow\", 0600, NULL, NULL,\nkernel/kcov_dataflow.c-1180-\t\t\t\t   \u0026kcov_df_fops);\n--\nkernel/kcov_dataflow.c-1192-}\nkernel/kcov_dataflow.c:1193:device_initcall(kcov_dataflow_init);\n--\nlib/Kconfig.debug=2222=config KCOV_DATAFLOW_ARGS\n--\nlib/Kconfig.debug-2229-\thelp\nlib/Kconfig.debug:2230:\t  Captures function arguments at entry via /sys/kernel/debug/kcov_dataflow.\nlib/Kconfig.debug-2231-\t  Struct pointer arguments are auto-expanded using compiler DebugInfo\n--\nlib/Kconfig.debug=2239=config KCOV_DATAFLOW_RET\n--\nlib/Kconfig.debug-2246-\thelp\nlib/Kconfig.debug:2247:\t  Captures function return values via /sys/kernel/debug/kcov_dataflow.\nlib/Kconfig.debug-2248-\t  Struct pointer returns are auto-expanded using compiler DebugInfo\n--\nscripts/Makefile.kcov=11=export RUSTFLAGS_KCOV := $(kcov-rflags-y)\n--\nscripts/Makefile.kcov-14-# its own Kconfig symbol, matching the #ifdef around the callback it emits calls\nscripts/Makefile.kcov:15:# to in kernel/kcov_dataflow.c (an instrumented object must never reference a\nscripts/Makefile.kcov-16-# callback that is not compiled in). Both variables are empty on a KCOV-only\n--\ntools/testing/selftests/kcov_dataflow/Makefile-2-#\ntools/testing/selftests/kcov_dataflow/Makefile:3:# kcov_dataflow selftests\ntools/testing/selftests/kcov_dataflow/Makefile-4-#\n--\ntools/testing/selftests/kcov_dataflow/README.rst=3=KCOV-Dataflow Selftests\n--\ntools/testing/selftests/kcov_dataflow/README.rst-5-\ntools/testing/selftests/kcov_dataflow/README.rst:6:Selftests for ``/sys/kernel/debug/kcov_dataflow`` (see\ntools/testing/selftests/kcov_dataflow/README.rst-7-Documentation/dev-tools/kcov-dataflow.rst).\n--\ntools/testing/selftests/kcov_dataflow/README.rst=36=need; with virtme-ng::\ntools/testing/selftests/kcov_dataflow/README.rst-37-\ntools/testing/selftests/kcov_dataflow/README.rst:38:    vng --build --config tools/testing/selftests/kcov_dataflow/config \\\ntools/testing/selftests/kcov_dataflow/README.rst-39-        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC\n--\ntools/testing/selftests/kcov_dataflow/README.rst=44=From the kernel tree, with the same toolchain variables::\n--\ntools/testing/selftests/kcov_dataflow/README.rst-46-    make LLVM=1 headers\ntools/testing/selftests/kcov_dataflow/README.rst:47:    make -C tools/testing/selftests TARGETS=kcov_dataflow \\\ntools/testing/selftests/kcov_dataflow/README.rst-48-        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC\n--\ntools/testing/selftests/kcov_dataflow/README.rst=58=On the target (root, debugfs mounted)::\n--\ntools/testing/selftests/kcov_dataflow/README.rst-60-    vng --user root --exec \\\ntools/testing/selftests/kcov_dataflow/README.rst:61:        \"tools/testing/selftests/kcov_dataflow/test_modules.py\"\ntools/testing/selftests/kcov_dataflow/README.rst:62:    tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\ntools/testing/selftests/kcov_dataflow/README.rst:63:    tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\ntools/testing/selftests/kcov_dataflow/README.rst-64-\ntools/testing/selftests/kcov_dataflow/README.rst:65:or, from an installed tree, ``run_kselftest.sh -c kcov_dataflow``.\ntools/testing/selftests/kcov_dataflow/README.rst-66-``test_modules.py -t \u003cmodule\u003e -C 8`` runs one module and echoes eight\n--\ntools/testing/selftests/kcov_dataflow/binderfs/Makefile-1-# SPDX-License-Identifier: GPL-2.0\ntools/testing/selftests/kcov_dataflow/binderfs/Makefile:2:# Standalone build of the binderfs test: make -C tools/testing/selftests/kcov_dataflow/binderfs\ntools/testing/selftests/kcov_dataflow/binderfs/Makefile-3-TEST_GEN_PROGS := binderfs_test\n--\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst=3=KCOV-Dataflow Selftests: binderfs\n--\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst-5-\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst:6:Exercises the binder driver via binderfs with kcov_dataflow recording\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst-7-active and verifies that argument records are captured at the binder\n--\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst=10=CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y); SKIPs without binderfs::\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst-11-\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst:12:  make -C tools/testing/selftests TARGETS=kcov_dataflow\ntools/testing/selftests/kcov_dataflow/binderfs/README.rst:13:  tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-2-/*\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:3: * binderfs selftest for kcov_dataflow\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-4- *\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:5: * Exercises the binder driver via binderfs with kcov_dataflow recording\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-6- * active, then verifies that function argument records were captured at\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-23-#include \u003clinux/android/binderfs.h\u003e\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:24:#include \u003clinux/kcov_dataflow.h\u003e\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-25-\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c=73=int main(void)\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-90-\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:91:\t/* Open kcov_dataflow */\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:92:\tdf_fd = open(\"/sys/kernel/debug/kcov_dataflow\", O_RDWR);\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-93-\tif (df_fd \u003c 0) {\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:94:\t\tprintf(\"not ok 1 cannot open kcov_dataflow\\n\");\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-95-\t\tcleanup_binderfs();\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-114-\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:115:\tprintf(\"ok 1 kcov_dataflow.binderfs_setup\\n\");\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-116-\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-147-\tif (total \u003e 0)\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:148:\t\tprintf(\"ok 2 kcov_dataflow.binderfs_captured # %lu words\\n\",\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-149-\t\t       (unsigned long)total);\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-150-\telse\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:151:\t\tprintf(\"not ok 2 kcov_dataflow.binderfs_captured # 0 words\\n\");\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-152-\n--\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-183-\tif (valid)\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:184:\t\tprintf(\"ok 3 kcov_dataflow.binderfs_valid_records\\n\");\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-185-\telse\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c:186:\t\tprintf(\"not ok 3 kcov_dataflow.binderfs_valid_records\\n\");\ntools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c-187-\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-2-/*\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c:3: * eight_struct_args_c.c - Verify kcov_dataflow captures struct pointer\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-4- * arguments with automatic field expansion.\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-25- *\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c:26: * Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct to invoke.\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-27- */\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c=519=static int __init eight_struct_args_init(void)\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-520-{\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c:521:\ttest_dir = debugfs_create_dir(\"kcov_dataflow_test\", NULL);\ntools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c-522-\tdebugfs_create_file(\"trigger_struct\", 0200, test_dir, NULL,\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-1-// SPDX-License-Identifier: GPL-2.0\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs:2://! Verify kcov_dataflow captures struct pointer arguments with automatic\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-3-//! field expansion for Rust #[repr(C)] structs.\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-19-//!\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs:20://! Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct_rust to invoke.\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-21-\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-31-\tauthors: [\"kcov-dataflow\"],\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs:32:\tdescription: \"Struct field expansion test for kcov_dataflow (Rust)\",\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-33-\tlicense: \"GPL\",\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs=613=    fn init(_module: \u0026'static ThisModule) -\u003e Result\u003cSelf\u003e {\n--\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-615-            kernel::bindings::debugfs_create_dir(\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs:616:                c_str!(\"kcov_dataflow_test\").as_char_ptr(),\ntools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs-617-                core::ptr::null_mut(),\n--\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-2-/*\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c:3: * rust_ffi_contract.c - Demonstrates kcov_dataflow detecting an FFI\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-4- * contract violation at a function boundary.\n--\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-9- *\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c:10: * kcov_dataflow captures:\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-11- *   [ENTRY] ffi_alloc_buf(alloc={.buffer=NULL, .data_size=0}, 256, 16, 1)\n--\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-16- *\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c:17: * Write to /sys/kernel/debug/kcov_dataflow_test/rust_ffi_trigger to run.\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-18- */\n--\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c=23=MODULE_LICENSE(\"GPL\");\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c:24:MODULE_DESCRIPTION(\"FFI contract violation detection via kcov_dataflow\");\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-25-\n--\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c=111=static int __init ffi_contract_init(void)\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-112-{\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c:113:\ttest_dir = debugfs_create_dir(\"kcov_dataflow_test\", NULL);\ntools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c-114-\tdebugfs_create_file(\"rust_ffi_trigger\", 0200, test_dir, NULL,\n--\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-10-//! User space publishes a buffer with KCOV_DF_REMOTE_ENABLE, writes to\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs:11://! /sys/kernel/debug/kcov_dataflow_test/trigger_kworker_remote, then reads\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-12-//! the captured records.\n--\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-29-\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs:30:// Extern bindings for kcov_dataflow remote API (kernel/kcov_dataflow.c)\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-31-unsafe extern \"C\" {\n--\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs=130=    fn run(this: Arc\u003cRemoteWork\u003e) {\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs:131:        // Enable remote kcov_dataflow capture for this kworker task.\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-132-        // SAFETY: FFI call to exported kernel symbol; no-op if no buffer published.\n--\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs=180=    fn init(_module: \u0026'static ThisModule) -\u003e Result\u003cSelf\u003e {\n--\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-182-            kernel::bindings::debugfs_create_dir(\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs:183:                c_str!(\"kcov_dataflow_test\").as_char_ptr(),\ntools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs-184-                core::ptr::null_mut(),\n--\ntools/testing/selftests/kcov_dataflow/test_modules.py-3-\"\"\"\ntools/testing/selftests/kcov_dataflow/test_modules.py:4:test_modules.py - run the kcov_dataflow test modules, one KTAP test each.\ntools/testing/selftests/kcov_dataflow/test_modules.py-5-\n--\ntools/testing/selftests/kcov_dataflow/test_modules.py=9=test passes only when the instrumented arguments, struct field expansions\ntools/testing/selftests/kcov_dataflow/test_modules.py:10:and return values came back intact through the kcov_dataflow buffer. The\ntools/testing/selftests/kcov_dataflow/test_modules.py-11-module's call tree is echoed as KTAP diagnostics.\n--\ntools/testing/selftests/kcov_dataflow/test_modules.py=16=Modules that were not built (no CONFIG_RUST, no toolchain) are reported as\ntools/testing/selftests/kcov_dataflow/test_modules.py:17:SKIP; a kernel without /sys/kernel/debug/kcov_dataflow skips everything.\ntools/testing/selftests/kcov_dataflow/test_modules.py-18-\"\"\"\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py-3-\"\"\"\ntools/testing/selftests/kcov_dataflow/trigger-view.py:4:trigger-view.py - Load a test module, trigger it with kcov_dataflow\ntools/testing/selftests/kcov_dataflow/trigger-view.py-5-recording active, then pretty-print the captured records.\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py=13=run_capture() does the work and is also what test_modules.py drives:\ntools/testing/selftests/kcov_dataflow/trigger-view.py:14:  1. Opens /sys/kernel/debug/kcov_dataflow, inits and mmaps the buffer\ntools/testing/selftests/kcov_dataflow/trigger-view.py-15-  2. Loads the module via finit_module() (its init noise is not recorded)\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py-20-  5. Disables recording and unloads the module\ntools/testing/selftests/kcov_dataflow/trigger-view.py:21:  6. Parses the records (layout: include/uapi/linux/kcov_dataflow.h)\ntools/testing/selftests/kcov_dataflow/trigger-view.py-22-\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py=42=import shutil\ntools/testing/selftests/kcov_dataflow/trigger-view.py-43-\ntools/testing/selftests/kcov_dataflow/trigger-view.py:44:# Constants -- must match include/uapi/linux/kcov_dataflow.h\ntools/testing/selftests/kcov_dataflow/trigger-view.py-45-DF_TYPE_CMP = 0xC\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py=90=KCOV_DF_REMOTE_DISABLE = _IO('d', 103)\ntools/testing/selftests/kcov_dataflow/trigger-view.py-91-\ntools/testing/selftests/kcov_dataflow/trigger-view.py:92:KCOV_DF_PATH = \"/sys/kernel/debug/kcov_dataflow\"\ntools/testing/selftests/kcov_dataflow/trigger-view.py-93-\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py-95-# writing to them runs the instrumented test functions.\ntools/testing/selftests/kcov_dataflow/trigger-view.py:96:TRIGGER_DIR = \"/sys/kernel/debug/kcov_dataflow_test\"\ntools/testing/selftests/kcov_dataflow/trigger-view.py-97-\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py=698=def main():\ntools/testing/selftests/kcov_dataflow/trigger-view.py-699-    parser = argparse.ArgumentParser(\ntools/testing/selftests/kcov_dataflow/trigger-view.py:700:        description=\"Load a test module with kcov_dataflow and view records\")\ntools/testing/selftests/kcov_dataflow/trigger-view.py-701-    parser.add_argument(\"module\", help=\"Test module name (e.g. eight_struct_args_c)\")\n--\ntools/testing/selftests/kcov_dataflow/trigger-view.py-717-        print(\"Build it first: make -C tools/testing/selftests \"\ntools/testing/selftests/kcov_dataflow/trigger-view.py:718:              \"TARGETS=kcov_dataflow LLVM=1 CC=clang\", file=sys.stderr)\ntools/testing/selftests/kcov_dataflow/trigger-view.py-719-        sys.exit(1)\n--\ntools/testing/selftests/kcov_dataflow/user_ioctl/Makefile-1-# SPDX-License-Identifier: GPL-2.0\ntools/testing/selftests/kcov_dataflow/user_ioctl/Makefile:2:# Standalone build of the ioctl test: make -C tools/testing/selftests/kcov_dataflow/user_ioctl\ntools/testing/selftests/kcov_dataflow/user_ioctl/Makefile-3-TEST_GEN_PROGS := user_ioctl\n--\ntools/testing/selftests/kcov_dataflow/user_ioctl/README.rst=8=a second fd failing with -EBUSY, and record validity after a syscall::\ntools/testing/selftests/kcov_dataflow/user_ioctl/README.rst-9-\ntools/testing/selftests/kcov_dataflow/user_ioctl/README.rst:10:  make -C tools/testing/selftests TARGETS=kcov_dataflow\ntools/testing/selftests/kcov_dataflow/user_ioctl/README.rst:11:  tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl\n--\ntools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c-2-/*\ntools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl.c:3: * kcov_dataflow_test.c - Selftest for /sys/kernel/debug/kcov_dataflow\n\n"
}

5/2 2026/09/03 13:17 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 90326
							output: 35
							thoughts: 520
						

Thoughts:
**Analyzing the KCOV-Dataflow Patch Series**

Okay, let's break this down. First, I need to understand what this patch is *doing*. It looks like it's implementing KCOV-Dataflow, specifically `kernel/kcov_dataflow.c`. This is a new feature for tracking function arguments, return values, and comparison values, and it's exposed through a per-task ring buffer accessible via `/sys/kernel/debug/kcov_dataflow`.

The implementation seems pretty involved, adding new ioctls: `KCOV_DF_INIT_TRACK`, `KCOV_DF_ENABLE`, `KCOV_DF_DISABLE`, `KCOV_DF_REMOTE_ENABLE`, `KCOV_DF_REMOTE_DISABLE`. There's also `mmap` functionality, shrinker logic, and callbacks related to process lifecycle (fork/exit: `kcov_dataflow_task_init`, `kcov_dataflow_task_exit`). The patch also implements comparison fan-out (`kcov_trace_cmp`) and remote start/stop capabilities (`kcov_df_remote_start`, `kcov_df_remote_stop`). Finally, there are compiler-emitted coverage callbacks (`__sanitizer_cov_trace_args`, `__sanitizer_cov_trace_ret`). That's a pretty comprehensive set of changes.

Now, the crucial question: is this code *reachable*? I'm seeing it's in the kernel core (`kernel/kcov_dataflow.c`, `kernel/kcov.c`, `kernel/fork.c`, `kernel/exit.c`), and it's definitely accessible via debugfs (`/sys/kernel/debug/kcov_dataflow`). User-space can interact with it via ioctls, `mmap`, etc. Plus, it hooks into fork/exit and comparison trace callbacks if enabled.  Based on this initial assessment, I'd say there's a definite path for fuzzing this. The answer to whether this is worth fuzzing is TRUE.

Finally, I need to know which Kconfig options are required to enable this. I'll need to check `lib/Kconfig.debug`. It looks like I'm dealing with options like `KCOV_DATAFLOW_ARGS`, `KCOV_DATAFLOW_RET`, `KCOV_DATAFLOW_INSTRUMENT_ALL`, and `KCOV_DATAFLOW_NO_INLINE`.  Those are the primary ones.  Of course, they depend on `KCOV`, `DEBUG_INFO`, and `DEBUG_FS` since it uses debugfs. I also need to make sure to check `KCOV_ENABLE_COMPARISONS` because it looks like that's needed for the comparison dataflow functionality. I'll need to examine `lib/Kconfig.debug` around line 2219 to see the full dependency tree.  I will confirm that these are all satisfied.



6/2 2026/09/03 13:17 tool read-file
0m Args:
{
  "File": "lib/Kconfig.debug",
  "FirstLine": 2210,
  "LineCount": 60
}

Results:
{
  "Contents": "2210:\t\t  KCOV uses preallocated areas to collect coverage from soft\n2211:\t\t  interrupts. This specifies the size of those areas in the\n2212:\t\t  number of unsigned long words.\n2213:\t\n2214:\tconfig KCOV_SELFTEST\n2215:\t\tbool \"Perform short selftests on boot\"\n2216:\t\tdepends on KCOV\n2217:\t\thelp\n2218:\t\t  Run short KCOV coverage collection selftests on boot.\n2219:\t\t  On test failure, causes the kernel to panic. Recommended to be\n2220:\t\t  enabled, ensuring critical functionality works as intended.\n2221:\t\n2222:\tconfig KCOV_DATAFLOW_ARGS\n2223:\t\tbool \"Enable KCOV dataflow: function argument capture\"\n2224:\t\tdepends on KCOV\n2225:\t\tdepends on CC_IS_CLANG\n2226:\t\tdepends on DEBUG_INFO\n2227:\t\tdepends on $(cc-option,-fsanitize-coverage=trace-args)\n2228:\t\tdepends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-args)\n2229:\t\thelp\n2230:\t\t  Captures function arguments at entry via /sys/kernel/debug/kcov_dataflow.\n2231:\t\t  Struct pointer arguments are auto-expanded using compiler DebugInfo\n2232:\t\t  metadata, recording individual field values at runtime.\n2233:\t\t  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.\n2234:\t\t  Requires clang with -fsanitize-coverage=trace-args support (and,\n2235:\t\t  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus\n2236:\t\t  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under\n2237:\t\t  \"Compile-time checks and compiler options\" to satisfy DEBUG_INFO.\n2238:\t\n2239:\tconfig KCOV_DATAFLOW_RET\n2240:\t\tbool \"Enable KCOV dataflow: return value capture\"\n2241:\t\tdepends on KCOV\n2242:\t\tdepends on CC_IS_CLANG\n2243:\t\tdepends on DEBUG_INFO\n2244:\t\tdepends on $(cc-option,-fsanitize-coverage=trace-ret)\n2245:\t\tdepends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-ret)\n2246:\t\thelp\n2247:\t\t  Captures function return values via /sys/kernel/debug/kcov_dataflow.\n2248:\t\t  Struct pointer returns are auto-expanded using compiler DebugInfo\n2249:\t\t  metadata, recording individual field values at runtime.\n2250:\t\t  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.\n2251:\t\t  Requires clang with -fsanitize-coverage=trace-ret support (and,\n2252:\t\t  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus\n2253:\t\t  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under\n2254:\t\t  \"Compile-time checks and compiler options\" to satisfy DEBUG_INFO.\n2255:\t\n2256:\tconfig KCOV_DATAFLOW_NO_INLINE\n2257:\t\tbool \"Disable inlining for dataflow-instrumented files\"\n2258:\t\tdepends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET\n2259:\t\thelp\n2260:\t\t  Adds -fno-inline to files instrumented with KCOV_DATAFLOW.\n2261:\t\t  This ensures every function boundary is preserved, giving\n2262:\t\t  complete argument visibility. Disable for lower overhead at the\n2263:\t\t  cost of losing argument records for inlined functions.\n2264:\t\n2265:\tconfig KCOV_DATAFLOW_INSTRUMENT_ALL\n2266:\t\tbool \"Instrument all kernel code with dataflow coverage\"\n2267:\t\tdepends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET\n2268:\t\thelp\n2269:\t\t  Instrument all kernel objects with trace-args/trace-ret\n"
}

7/2 2026/09/03 13:17 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 92648
							output: 38
							thoughts: 53
						

Thoughts:
Okay, let's dive into this.

**Focusing Fuzzing Efforts in `kcov_dataflow.c`**

Alright, the objective is clear: I need to pinpoint the most effective areas for fuzzing within the `kernel/kcov_dataflow.c` file. My immediate thought is to zero in on any entry points or functions that aren't widely used across the kernel. Identifying these "non-ubiquitous" elements will likely yield the best results for bug hunting, as they might represent less-tested code paths. So, first step is to crack open that file and start reading it to understand the functions within. I could also dive deeper and inspect the functions rather than read it. It's a quick analysis to find that sweet spot for focused fuzzing.



8/2 2026/09/03 13:17 tool read-file
0m Args:
{
  "File": "kernel/kcov_dataflow.c",
  "FirstLine": 730,
  "LineCount": 100
}

Results:
{
  "Contents": " 730:\t\tt-\u003ekcov_df_enabled = false;\n 731:\t\tt-\u003ekcov_df_area = NULL;\n 732:\t\tt-\u003ekcov_df_size = 0;\n 733:\t\tt-\u003ekcov_df = NULL;\n 734:\t\n 735:\t\tmutex_lock(\u0026df-\u003elock);\n 736:\t\tWARN_ON_ONCE(df-\u003et != t);\n 737:\t\tdf-\u003et = NULL;\n 738:\t\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);\n 739:\t\tmutex_unlock(\u0026df-\u003elock);\n 740:\t\tkcov_df_put(df);\n 741:\t}\n 742:\t\n 743:\t/* File operations for /sys/kernel/debug/kcov_dataflow */\n 744:\t\n 745:\tstatic int kcov_df_open(struct inode *inode, struct file *filep)\n 746:\t{\n 747:\t\tstruct kcov_dataflow *df;\n 748:\t\n 749:\t\tdf = kzalloc_obj(struct kcov_dataflow, GFP_KERNEL);\n 750:\t\tif (!df)\n 751:\t\t\treturn -ENOMEM;\n 752:\t\tmutex_init(\u0026df-\u003elock);\n 753:\t\trefcount_set(\u0026df-\u003erefcount, 1);\t/* the open fd's reference */\n 754:\t\tfilep-\u003eprivate_data = df;\n 755:\t\treturn nonseekable_open(inode, filep);\n 756:\t}\n 757:\t\n 758:\t/*\n 759:\t * Unwire the local session that @current holds on @df. Caller holds df-\u003elock\n 760:\t * and must drop the session's reference with kcov_df_put() after unlocking.\n 761:\t */\n 762:\tstatic void kcov_df_disable_local(struct kcov_dataflow *df)\n 763:\t{\n 764:\t\tlockdep_assert_held(\u0026df-\u003elock);\n 765:\t\tWARN_ON_ONCE(df-\u003et != current || current-\u003ekcov_df != df);\n 766:\t\n 767:\t\tcurrent-\u003ekcov_df_enabled = false;\n 768:\t\tcurrent-\u003ekcov_df_area = NULL;\n 769:\t\tcurrent-\u003ekcov_df_size = 0;\n 770:\t\tcurrent-\u003ekcov_df = NULL;\n 771:\t\tdf-\u003et = NULL;\n 772:\t\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);\n 773:\t}\n 774:\t\n 775:\tstatic int kcov_df_close(struct inode *inode, struct file *filep)\n 776:\t{\n 777:\t\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\n 778:\t\tbool put_session = false;\n 779:\t\n 780:\t\t/* Unpublish from remote hash: no new users can start */\n 781:\t\tkcov_df_remote_unpublish(df);\n 782:\t\n 783:\t\tmutex_lock(\u0026df-\u003elock);\n 784:\t\tkcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);\n 785:\t\t/*\n 786:\t\t * Only the enabled task can unwire its own session. If another task\n 787:\t\t * (a sibling thread, a fork()ed child, an SCM_RIGHTS recipient) does\n 788:\t\t * the final close(), the enabled task keeps its reference and keeps\n 789:\t\t * collecting until it exits, exactly like mainline kcov.\n 790:\t\t */\n 791:\t\tif (df-\u003et == current) {\n 792:\t\t\tkcov_df_disable_local(df);\n 793:\t\t\tput_session = true;\n 794:\t\t}\n 795:\t\tmutex_unlock(\u0026df-\u003elock);\n 796:\t\n 797:\t\tif (put_session)\n 798:\t\t\tkcov_df_put(df);\n 799:\t\t/*\n 800:\t\t * Drop the fd's reference. If remote workers or the enabled task still\n 801:\t\t * hold refs, the LAST of them frees -\u003earea via kcov_df_put() -- no drain\n 802:\t\t * loop, no lost-decrement wedge. The hash entry was already unpublished\n 803:\t\t * above, so no new remote user can start on this object.\n 804:\t\t */\n 805:\t\tkcov_df_put(df);\n 806:\t\treturn 0;\n 807:\t}\n 808:\t\n 809:\tstatic int kcov_df_mmap(struct file *filep, struct vm_area_struct *vma)\n 810:\t{\n 811:\t\tstruct kcov_dataflow *df = filep-\u003eprivate_data;\n 812:\t\tunsigned long size, off;\n 813:\t\tstruct page *page;\n 814:\t\tvoid *area;\n 815:\t\tint res = 0;\n 816:\t\n 817:\t\tmutex_lock(\u0026df-\u003elock);\n 818:\t\tsize = df-\u003esize * sizeof(u64);\n 819:\t\tif (!df-\u003earea || vma-\u003evm_pgoff != 0 ||\n 820:\t\t    vma-\u003evm_end - vma-\u003evm_start != size) {\n 821:\t\t\tres = -EINVAL;\n 822:\t\t\tgoto out;\n 823:\t\t}\n 824:\t\tarea = df-\u003earea;\n 825:\t\tmutex_unlock(\u0026df-\u003elock);\n 826:\t\n 827:\t\tvm_flags_set(vma, VM_DONTEXPAND);\n 828:\t\tfor (off = 0; off \u003c size; off += PAGE_SIZE) {\n 829:\t\t\tpage = vmalloc_to_page(area + off);\n"
}

9/2 2026/09/03 13:17 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 94511
							output: 388
							thoughts: 781
						

Thoughts:
**Assessment of kcov_dataflow.c for Fuzzing**

Okay, let's run through this. First, I need to make sure I've got a comprehensive list of functions to review within `kernel/kcov_dataflow.c`. I need to ensure I've covered everything related to the dataflow functionality, so I'll double-check: `kcov_df_ioctl`, `kcov_df_open`, `kcov_df_mmap`, `kcov_df_close`, `kcov_df_remote_start`, `kcov_df_remote_stop`, `kcov_df_trace_cmp`, `__sanitizer_cov_trace_args`, `__sanitizer_cov_trace_ret`, `kcov_dataflow_task_init`, `kcov_dataflow_task_exit`, `kcov_df_scratch_shrink_count`, `kcov_df_scratch_shrink_scan`, `kcov_df_merge`, `kcov_df_write`, and `kcov_df_reserve`.

Right, now, I also need to verify that I'm not missing anything else that might be related, specifically those comparison traces. Let's make sure I've got `kcov_trace_cmp` and `write_comp_data` covered. I know `kcov_trace_cmp` is static inline and defined in `include/linux/kcov.h`, so I'll keep that in mind. `write_comp_data` will be in `kernel/kcov.c`.

Okay, now let's circle back and ensure I've met all the requirements. WorthFuzzing is true, which is good. The reasoning, concise as it is, fits the bill. The `FocusSymbols` list is critical; I'll double-check that this is accurate: It needs to include `kcov_df_ioctl`, `kcov_df_mmap`, `kcov_df_open`, `kcov_df_close`, `kcov_df_remote_start`, `kcov_df_remote_stop`, `kcov_df_trace_cmp`, `__sanitizer_cov_trace_args`, `__sanitizer_cov_trace_ret`, `kcov_dataflow_task_exit`, `kcov_dataflow_task_init`, `kcov_df_scratch_shrink_scan`, `kcov_df_scratch_shrink_count` and `write_comp_data`. That should be complete.

Now for the required `EnableConfigs`: `KCOV`, `KCOV_DATAFLOW_ARGS`, `KCOV_DATAFLOW_RET`, `KCOV_DATAFLOW_INSTRUMENT_ALL`, `KCOV_ENABLE_COMPARISONS`, and `DEBUG_FS`. Let's do a quick sanity check to ensure I'm not missing any pre-requisites.

So, `KCOV`, `KCOV_DATAFLOW_ARGS`, `KCOV_DATAFLOW_RET`, and `DEBUG_FS` are all required, good. Then, `KCOV_DATAFLOW_INSTRUMENT_ALL` and `KCOV_ENABLE_COMPARISONS` are optional config flags, which is fine, but they're still relevant.

Lastly, I should double-check the configuration options in `lib/Kconfig.debug`. Just to be safe, I'll review those again: `KCOV`, `KCOV_DATAFLOW_ARGS`, `KCOV_DATAFLOW_RET`, `KCOV_DATAFLOW_INSTRUMENT_ALL`, `KCOV_DATAFLOW_NO_INLINE`, `DEBUG_INFO`, and `DEBUG_FS`. Yup, looks like all the critical configs are accounted for.

Alright, everything seems to be complete and fully validated. I've covered all the bases and there's nothing else that I need to clarify. Let's call `set-results`.



10/2 2026/09/03 13:17 tool set-results
0m Args:
{
  "EnableConfigs": [
    "KCOV",
    "KCOV_DATAFLOW_ARGS",
    "KCOV_DATAFLOW_RET",
    "KCOV_DATAFLOW_INSTRUMENT_ALL",
    "KCOV_DATAFLOW_NO_INLINE",
    "KCOV_ENABLE_COMPARISONS",
    "DEBUG_FS",
    "DEBUG_INFO"
  ],
  "FocusSymbols": [
    "kcov_df_ioctl",
    "kcov_df_mmap",
    "kcov_df_open",
    "kcov_df_close",
    "kcov_df_remote_start",
    "kcov_df_remote_stop",
    "kcov_df_trace_cmp",
    "__sanitizer_cov_trace_args",
    "__sanitizer_cov_trace_ret",
    "kcov_dataflow_task_exit",
    "kcov_dataflow_task_init",
    "kcov_df_scratch_shrink_scan",
    "kcov_df_scratch_shrink_count",
    "write_comp_data"
  ],
  "Reasoning": "The patch introduces KCOV-Dataflow, adding a new debugfs interface (/sys/kernel/debug/kcov_dataflow), ioctls (KCOV_DF_INIT_TRACK, KCOV_DF_ENABLE, KCOV_DF_DISABLE, KCOV_DF_REMOTE_ENABLE, KCOV_DF_REMOTE_DISABLE), mmap, remote kworker tracing, task lifecycle tracking in fork/exit, memory shrinker management, and comparison fan-out. This introduces significant new kernel code and state management in reachable subsystems that warrants fuzzing.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "KCOV",
    "KCOV_DATAFLOW_ARGS",
    "KCOV_DATAFLOW_RET",
    "KCOV_DATAFLOW_INSTRUMENT_ALL",
    "KCOV_DATAFLOW_NO_INLINE",
    "KCOV_ENABLE_COMPARISONS",
    "DEBUG_FS",
    "DEBUG_INFO"
  ],
  "FocusSymbols": [
    "kcov_df_ioctl",
    "kcov_df_mmap",
    "kcov_df_open",
    "kcov_df_close",
    "kcov_df_remote_start",
    "kcov_df_remote_stop",
    "kcov_df_trace_cmp",
    "__sanitizer_cov_trace_args",
    "__sanitizer_cov_trace_ret",
    "kcov_dataflow_task_exit",
    "kcov_dataflow_task_init",
    "kcov_df_scratch_shrink_scan",
    "kcov_df_scratch_shrink_count",
    "write_comp_data"
  ],
  "Reasoning": "The patch introduces KCOV-Dataflow, adding a new debugfs interface (/sys/kernel/debug/kcov_dataflow), ioctls (KCOV_DF_INIT_TRACK, KCOV_DF_ENABLE, KCOV_DF_DISABLE, KCOV_DF_REMOTE_ENABLE, KCOV_DF_REMOTE_DISABLE), mmap, remote kworker tracing, task lifecycle tracking in fork/exit, memory shrinker management, and comparison fan-out. This introduces significant new kernel code and state management in reachable subsystems that warrants fuzzing.",
  "WorthFuzzing": true
}

11/1 2026/09/03 13:17 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

12/2 2026/09/03 13:17 agent kmsan-evaluator
0m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces KCOV-Dataflow, adding new infrastructure to capture function arguments, return values, and comparison operands into mmap-able per-task and remote buffers.\n\nAnalysis for uninitialized memory risks:\n1. Heap objects (`struct kcov_dataflow`, `struct kcov_df_remote`) are allocated using `kzalloc_obj()`, ensuring all fields are zero-initialized.\n2. The user-accessible dataflow buffer is allocated via `vmalloc_user()`, which automatically zeroes memory pages.\n3. In data collection callbacks (`kcov_df_write()`, `kcov_df_trace_cmp()`), all record fields, headers, and values are explicitly initialized (`val = 0`, etc.) before being populated via safe accessors (`copy_from_kernel_nofault()`, `get_kernel_nofault()`).\n4. Per-task dataflow fields in `task_struct` are explicitly initialized in `kcov_dataflow_task_init()`.\n5. No uninitialized memory or struct padding is copied to user space or evaluated in branching decisions.\n6. `kernel/kcov_dataflow.c` explicitly disables KMSAN instrumentation (`KMSAN_SANITIZE_kcov_dataflow.o := n`).\n7. Potential concurrency, bounds, and refcounting issues fall strictly under the domain of KASAN, LOCKDEP, and standard kernel debugging facilities.\n\nTherefore, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit add0fd47998ebfbe54b3752a7ef1aa9d7ee4fc05
Author: syz-cluster <triage@syzkaller.com>
Date:   Thu Sep 3 13:17:20 2026 +0000

    syz-cluster: applied patch under review

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 a9245d827ddb6..057f4e14ff46e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14077,7 +14077,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
diff --git a/include/linux/kcov.h b/include/linux/kcov.h
index 895b761b2db15..55e1405bc4bc4 100644
--- a/include/linux/kcov.h
+++ b/include/linux/kcov.h
@@ -3,6 +3,7 @@
 #define _LINUX_KCOV_H
 
 #include <linux/sched.h>
+#include <linux/jump_label.h>
 #include <uapi/linux/kcov.h>
 
 struct task_struct;
@@ -28,6 +29,14 @@ enum kcov_mode {
 void kcov_task_init(struct task_struct *t);
 void kcov_task_exit(struct task_struct *t);
 
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+void kcov_dataflow_task_init(struct task_struct *t);
+void kcov_dataflow_task_exit(struct task_struct *t);
+#else
+static inline void kcov_dataflow_task_init(struct task_struct *t) {}
+static inline void kcov_dataflow_task_exit(struct task_struct *t) {}
+#endif
+
 #define kcov_prepare_switch(t)			\
 do {						\
 	(t)->kcov_mode |= KCOV_IN_CTXSW;	\
@@ -43,6 +52,29 @@ void kcov_remote_start(u64 handle);
 void kcov_remote_stop(void);
 struct kcov_common_handle_id kcov_common_handle(void);
 
+/*
+ * Validate a remote handle: it must be a well-formed kcov_remote_handle()
+ * encoding, and each caller states which subsystem/instance combinations it
+ * accepts. Shared by KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE so both
+ * collectors take handles from the same partitioned namespace.
+ */
+static inline bool kcov_check_handle(u64 handle, bool common_valid,
+				     bool uncommon_valid, bool zero_valid)
+{
+	if (handle & ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))
+		return false;
+	switch (handle & KCOV_SUBSYSTEM_MASK) {
+	case KCOV_SUBSYSTEM_COMMON:
+		return (handle & KCOV_INSTANCE_MASK) ?
+			common_valid : zero_valid;
+	case KCOV_SUBSYSTEM_USB:
+		return uncommon_valid;
+	default:
+		return false;
+	}
+	return false;
+}
+
 static inline void kcov_remote_start_common(struct kcov_common_handle_id id)
 {
 	kcov_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));
@@ -107,4 +139,88 @@ static inline void kcov_remote_start_usb_softirq(u64 id) {}
 static inline void kcov_remote_stop_softirq(void) {}
 
 #endif /* CONFIG_KCOV */
+
+/*
+ * kcov_dataflow remote API. The collector is a separate object from mainline
+ * kcov and is only linked in when at least one of the two capture modes is
+ * configured (see kernel/Makefile), so gate the declarations the same way
+ * kcov_dataflow_task_init() above is gated; a caller that brackets a region for
+ * both collectors then still builds on a KCOV-only config.
+ */
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+void kcov_df_remote_start(u64 handle);
+void kcov_df_remote_stop(void);
+#else
+static inline void kcov_df_remote_start(u64 handle) {}
+static inline void kcov_df_remote_stop(void) {}
+#endif
+
+/*
+ * Handle-typed wrapper mirroring kcov_remote_start_common(), so a subsystem that
+ * already routes its mainline kcov remote sections by struct
+ * kcov_common_handle_id can open a dataflow section on the very same handle
+ * without knowing how it is encoded. The two collectors keep separate per-task
+ * state and separate handle tables, so a section of each may be nested around
+ * the same region; user space registers the identical handle value with
+ * KCOV_REMOTE_ENABLE and KCOV_DF_REMOTE_ENABLE to collect both.
+ *
+ * Unlike kcov_remote_start(), the dataflow section may only be opened from
+ * sleepable task context: kcov_df_remote_start()/kcov_df_remote_stop() take a
+ * mutex and may allocate or free the worker's scratch area. Both are no-ops in
+ * softirq/hardirq context, so a softirq-bracketing call site collects no
+ * dataflow records rather than misbehaving. A call site that is only
+ * sometimes atomic (spinlock held, preemption or irqs disabled) must not use
+ * this wrapper; CONFIG_DEBUG_ATOMIC_SLEEP reports such a caller.
+ *
+ * Without CONFIG_KCOV the handle carries no value (see struct
+ * kcov_common_handle_id), and dataflow depends on KCOV, so this is a no-op.
+ */
+#ifdef CONFIG_KCOV
+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)
+{
+	kcov_df_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, id.val));
+}
+#else
+static inline void kcov_df_remote_start_common(struct kcov_common_handle_id id)
+{
+}
+#endif
+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) && \
+	(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))
+/*
+ * CONFIG_KCOV_ENABLE_COMPARISONS provides ONE trace-cmp instrumentation shared by
+ * mainline kcov and kcov-dataflow. kcov.c's __sanitizer_cov_trace_cmp*() callbacks
+ * route each operand pair through kcov_trace_cmp() below, which fans it out:
+ * mainline kcov always sees it (write_comp_data() records only when the task is
+ * in KCOV_MODE_TRACE_CMP), and a task with a live dataflow session gets a copy in
+ * its dataflow buffer as well. The two collectors are independent fds with no
+ * cross-exclusion, so a task may collect for both at once, and a dataflow-side
+ * drop (inert context, full buffer) never costs mainline kcov a record. kcov.c
+ * never references the dataflow side, one cmp symbol feeds both collectors, and
+ * there is no separate df_cmp symbol or compiler change.
+ *
+ * The dataflow branch is gated by a static key so that, while no dataflow session
+ * is live, this whole-kernel hot path is a patched-out NOP that costs nothing on
+ * top of mainline write_comp_data() (kcov_df_cmp_key is inc'd on dataflow enable
+ * in kcov_dataflow.c).
+ */
+DECLARE_STATIC_KEY_FALSE(kcov_df_cmp_key);
+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);
+void kcov_df_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip);
+static inline notrace void
+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)
+{
+	write_comp_data(type, arg1, arg2, ip);			/* mainline kcov */
+	if (static_branch_unlikely(&kcov_df_cmp_key) && current->kcov_df_enabled)
+		kcov_df_trace_cmp(type, arg1, arg2, ip);	/* kcov-dataflow */
+}
+#elif defined(CONFIG_KCOV_ENABLE_COMPARISONS)
+/* Comparisons without a dataflow build: route straight to mainline kcov. */
+void write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip);
+static inline notrace void
+kcov_trace_cmp(u64 type, u64 arg1, u64 arg2, u64 ip)
+{
+	write_comp_data(type, arg1, arg2, ip);
+}
+#endif
 #endif /* _LINUX_KCOV_H */
diff --git a/include/linux/sched.h b/include/linux/sched.h
index eb12ff4cea6c2..589aa57e19124 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1553,6 +1553,40 @@ struct task_struct {
 	/* KCOV sequence number: */
 	int				kcov_sequence;
 
+#if defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET)
+	/*
+	 * KCOV dataflow per-task record sequence counter (24 bits used) plus,
+	 * in bit 31, the recursion guard held while a callback is running:
+	 */
+	u32				kcov_df_seq;
+
+	/* KCOV dataflow: separate buffer for trace-args/trace-ret */
+	unsigned int			kcov_df_size;
+	void				*kcov_df_area;
+	bool				kcov_df_enabled;
+
+	/*
+	 * The kcov_dataflow object this task's session belongs to, NULL when
+	 * no session is active. The task holds a reference on it for the whole
+	 * session, whether local (KCOV_DF_ENABLE, mirrors t->kcov) or remote
+	 * (kcov_df_remote_start()), so the buffer can never be freed under an
+	 * instrumented callback and both task exit and kcov_df_remote_stop()
+	 * reach the exact object without a hash lookup.
+	 */
+	struct kcov_dataflow		*kcov_df;
+
+	/*
+	 * Nesting depth of kcov_df_remote_start() on this task: 0 while no
+	 * remote session is active (including during a local session), 1 for
+	 * a normal bracketed work item. If a buggy caller nests, the inner
+	 * start()s only bump this and the inner stop()s only decrement it, so
+	 * the OUTER session (buffer + ref) is torn down exactly once, at the
+	 * outermost stop -- never early, which would otherwise drop the ref
+	 * and free the buffer out from under the still-running outer worker.
+	 */
+	int				kcov_df_remote_depth;
+#endif
+
 	/* Collect coverage from softirq context: */
 	unsigned int			kcov_softirq;
 
diff --git a/include/uapi/linux/kcov_dataflow.h b/include/uapi/linux/kcov_dataflow.h
new file mode 100644
index 0000000000000..db3112a45832c
--- /dev/null
+++ b/include/uapi/linux/kcov_dataflow.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_KCOV_DATAFLOW_H
+#define _LINUX_KCOV_DATAFLOW_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+/*
+ * User space ABI of /sys/kernel/debug/kcov_dataflow, see
+ * Documentation/dev-tools/kcov-dataflow.rst.
+ *
+ * KCOV_DF_INIT_TRACK takes the buffer size in u64 words by value (same
+ * convention as KCOV_INIT_TRACE). KCOV_DF_REMOTE_ENABLE takes a pointer to a
+ * __u64 remote handle encoded with kcov_remote_handle() (linux/kcov.h), so the
+ * full 64-bit value survives 32-bit and compat callers.
+ */
+#define KCOV_DF_INIT_TRACK	_IOR('d', 1, unsigned long)
+#define KCOV_DF_ENABLE		_IO('d', 100)
+#define KCOV_DF_DISABLE		_IO('d', 101)
+#define KCOV_DF_REMOTE_ENABLE	_IOW('d', 102, __u64)
+#define KCOV_DF_REMOTE_DISABLE	_IO('d', 103)
+
+/*
+ * Buffer layout (all u64 words):
+ *
+ *   area[0]                number of record words written after area[0]
+ *   area[1 + n ..]         records, back to back, each:
+ *
+ *     [0] header           see KCOV_DF_HDR_* below
+ *     [1] pc               instrumented location; KASLR offset removed, like
+ *                          the PCs mainline kcov records
+ *     [2] ENTRY/RET: the traced value's address (full pointer); may be a
+ *                    NULL/ERR_PTR value the callee received, in which case the
+ *                    value words hold KCOV_DF_MAGIC_BAD
+ *         CMP:       comparison type, KCOV_CMP_SIZE()/KCOV_CMP_CONST bits
+ *                    (linux/kcov.h)
+ *     [3 .. 3 + nvals)     value words: the scalar (nvals == 1), the expanded
+ *                          struct fields, or the two CMP operands (nvals == 2)
+ *
+ * The header packs:
+ *
+ *   bits  0..23  per-task record sequence number
+ *   bits 28..31  record type, KCOV_DF_TYPE_*
+ *   bits 32..47  nvals, the number of value words that follow word [2]
+ *   bits 48..55  ENTRY/RET: size in bytes of the traced argument/return value
+ *                (clamped to 255)
+ *   bits 56..63  ENTRY: argument index (clamped to 255); RET: 0
+ *
+ * A consumer walks the buffer as
+ *
+ *	pos = 1;
+ *	while (pos < 1 + area[0]) {
+ *		hdr = area[pos];
+ *		nvals = KCOV_DF_HDR_NVALS(hdr);
+ *		...
+ *		pos += KCOV_DF_RECORD_WORDS(nvals);
+ *	}
+ *
+ * area[0] never exceeds the buffer size minus one, and every counted word has
+ * been written, so the walk above stays inside the mapping.
+ */
+#define KCOV_DF_TYPE_CMP	0xC
+#define KCOV_DF_TYPE_ENTRY	0xE
+#define KCOV_DF_TYPE_RET	0xF
+
+#define KCOV_DF_HDR_SEQ_MASK	0x00FFFFFFULL
+#define KCOV_DF_HDR_TYPE_SHIFT	28
+#define KCOV_DF_HDR_TYPE_MASK	0xFULL
+#define KCOV_DF_HDR_NVALS_SHIFT	32
+#define KCOV_DF_HDR_NVALS_MASK	0xFFFFULL
+#define KCOV_DF_HDR_SIZE_SHIFT	48
+#define KCOV_DF_HDR_SIZE_MASK	0xFFULL
+#define KCOV_DF_HDR_ARGIDX_SHIFT 56
+#define KCOV_DF_HDR_ARGIDX_MASK	0xFFULL
+
+#define KCOV_DF_HDR_SEQ(h)	((h) & KCOV_DF_HDR_SEQ_MASK)
+#define KCOV_DF_HDR_TYPE(h)	(((h) >> KCOV_DF_HDR_TYPE_SHIFT) & KCOV_DF_HDR_TYPE_MASK)
+#define KCOV_DF_HDR_NVALS(h)	(((h) >> KCOV_DF_HDR_NVALS_SHIFT) & KCOV_DF_HDR_NVALS_MASK)
+#define KCOV_DF_HDR_SIZE(h)	(((h) >> KCOV_DF_HDR_SIZE_SHIFT) & KCOV_DF_HDR_SIZE_MASK)
+#define KCOV_DF_HDR_ARGIDX(h)	(((h) >> KCOV_DF_HDR_ARGIDX_SHIFT) & KCOV_DF_HDR_ARGIDX_MASK)
+
+/* Words per record: header, pc, pointer/cmp-type, then the value words. */
+#define KCOV_DF_RECORD_HDR_WORDS	3
+#define KCOV_DF_RECORD_WORDS(nvals)	(KCOV_DF_RECORD_HDR_WORDS + (nvals))
+
+/* Largest nvals a record can carry; longer field lists are truncated. */
+#define KCOV_DF_MAX_VALS	KCOV_DF_HDR_NVALS_MASK
+
+/* Value word written when the traced pointer or a field could not be read. */
+#define KCOV_DF_MAGIC_BAD	0xBADADD85ULL
+
+#endif /* _LINUX_KCOV_DATAFLOW_H */
diff --git a/kernel/Makefile b/kernel/Makefile
index 1e1a31673577d..307b7fd1e1f96 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -44,6 +44,12 @@ KCSAN_SANITIZE_kcov.o := n
 UBSAN_SANITIZE_kcov.o := n
 KMSAN_SANITIZE_kcov.o := n
 
+KCOV_INSTRUMENT_kcov_dataflow.o := n
+KASAN_SANITIZE_kcov_dataflow.o := n
+KCSAN_SANITIZE_kcov_dataflow.o := n
+UBSAN_SANITIZE_kcov_dataflow.o := n
+KMSAN_SANITIZE_kcov_dataflow.o := n
+
 CONTEXT_ANALYSIS_kcov.o := y
 CFLAGS_kcov.o := $(call cc-option, -fno-conserve-stack) -fno-stack-protector
 
@@ -98,6 +104,9 @@ obj-$(CONFIG_AUDIT) += audit.o auditfilter.o
 obj-$(CONFIG_AUDITSYSCALL) += auditsc.o audit_watch.o audit_fsnotify.o audit_tree.o
 obj-$(CONFIG_GCOV_KERNEL) += gcov/
 obj-$(CONFIG_KCOV) += kcov.o
+ifneq ($(CONFIG_KCOV_DATAFLOW_ARGS)$(CONFIG_KCOV_DATAFLOW_RET),)
+obj-y += kcov_dataflow.o
+endif
 obj-$(CONFIG_KPROBES) += kprobes.o
 obj-$(CONFIG_FAIL_FUNCTION) += fail_function.o
 obj-$(CONFIG_KGDB) += debug/
diff --git a/kernel/exit.c b/kernel/exit.c
index 97686af895013..8881661d635ba 100644
--- a/kernel/exit.c
+++ b/kernel/exit.c
@@ -939,6 +939,7 @@ void __noreturn do_exit(long code)
 		kthread_do_exit(kthread, code);
 
 	kcov_task_exit(tsk);
+	kcov_dataflow_task_exit(tsk);
 	kmsan_task_exit(tsk);
 
 	synchronize_group_exit(tsk, code);
diff --git a/kernel/fork.c b/kernel/fork.c
index 22283bf849e15..14d4fe5c7909b 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -985,6 +985,7 @@ static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
 	tsk->worker_private = NULL;
 
 	kcov_task_init(tsk);
+	kcov_dataflow_task_init(tsk);
 	kmsan_task_create(tsk);
 	kmap_local_fork(tsk);
 
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 35420f0ac524d..cac9b69e197ed 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -232,7 +232,14 @@ void notrace __sanitizer_cov_trace_pc(void)
 EXPORT_SYMBOL(__sanitizer_cov_trace_pc);
 
 #ifdef CONFIG_KCOV_ENABLE_COMPARISONS
-static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
+/*
+ * Mainline kcov comparison writer: appends to the task's own kcov buffer, and
+ * only in KCOV_MODE_TRACE_CMP. The fan-out that also feeds the kcov-dataflow
+ * buffer lives in kcov_trace_cmp() in <linux/kcov.h>, so kcov.c never references
+ * the dataflow side itself. This writer is only non-static so that header helper
+ * (which the cmp callbacks below call) can reach it.
+ */
+void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
 {
 	struct task_struct *t;
 	u64 *area;
@@ -267,55 +274,59 @@ static void notrace write_comp_data(u64 type, u64 arg1, u64 arg2, u64 ip)
 	}
 }
 
+/*
+ * The __sanitizer_cov_trace_cmp*() callbacks stay here in kcov.c (one shared,
+ * compiler-emitted symbol per comparison -- no separate df_cmp symbol, no
+ * compiler change). Each routes its operand pair through kcov_trace_cmp()
+ * (defined in <linux/kcov.h>), which records into mainline kcov and, when this
+ * task has a dataflow session, into kcov-dataflow too. kcov.c never names the
+ * dataflow side; that fan-out lives entirely in the header.
+ */
 void notrace __sanitizer_cov_trace_cmp1(u8 arg1, u8 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(0), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp1);
 
 void notrace __sanitizer_cov_trace_cmp2(u16 arg1, u16 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(1), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp2);
 
 void notrace __sanitizer_cov_trace_cmp4(u32 arg1, u32 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(2), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp4);
 
 void notrace __sanitizer_cov_trace_cmp8(kcov_u64 arg1, kcov_u64 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(3), arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_cmp8);
 
 void notrace __sanitizer_cov_trace_const_cmp1(u8 arg1, u8 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(0) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp1);
 
 void notrace __sanitizer_cov_trace_const_cmp2(u16 arg1, u16 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(1) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp2);
 
 void notrace __sanitizer_cov_trace_const_cmp4(u32 arg1, u32 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(2) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp4);
 
 void notrace __sanitizer_cov_trace_const_cmp8(kcov_u64 arg1, kcov_u64 arg2)
 {
-	write_comp_data(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2,
-			_RET_IP_);
+	kcov_trace_cmp(KCOV_CMP_SIZE(3) | KCOV_CMP_CONST, arg1, arg2, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_const_cmp8);
 
@@ -344,7 +355,7 @@ void notrace __sanitizer_cov_trace_switch(kcov_u64 val, void *arg)
 		return;
 	}
 	for (i = 0; i < count; i++)
-		write_comp_data(type, cases[i + 2], val, _RET_IP_);
+		kcov_trace_cmp(type, cases[i + 2], val, _RET_IP_);
 }
 EXPORT_SYMBOL(__sanitizer_cov_trace_switch);
 #endif /* ifdef CONFIG_KCOV_ENABLE_COMPARISONS */
@@ -587,23 +598,6 @@ static void kcov_fault_in_area(struct kcov *kcov)
 		READ_ONCE(area[offset]);
 }
 
-static inline bool kcov_check_handle(u64 handle, bool common_valid,
-				bool uncommon_valid, bool zero_valid)
-{
-	if (handle & ~(KCOV_SUBSYSTEM_MASK | KCOV_INSTANCE_MASK))
-		return false;
-	switch (handle & KCOV_SUBSYSTEM_MASK) {
-	case KCOV_SUBSYSTEM_COMMON:
-		return (handle & KCOV_INSTANCE_MASK) ?
-			common_valid : zero_valid;
-	case KCOV_SUBSYSTEM_USB:
-		return uncommon_valid;
-	default:
-		return false;
-	}
-	return false;
-}
-
 static int kcov_ioctl_locked(struct kcov *kcov, unsigned int cmd,
 			     unsigned long arg)
 	__must_hold(&kcov->lock)
diff --git a/kernel/kcov_dataflow.c b/kernel/kcov_dataflow.c
new file mode 100644
index 0000000000000..641d6bc763864
--- /dev/null
+++ b/kernel/kcov_dataflow.c
@@ -0,0 +1,1193 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KCOV Dataflow: per-task function argument/return value capture.
+ *
+ * Exposes /sys/kernel/debug/kcov_dataflow, completely independent from
+ * /sys/kernel/debug/kcov. Own buffer, own ioctl, own mmap.
+ *
+ * The user-visible ABI:
+ *
+ * ioctls, the record layout and the header bit fields, is defined in
+ * <uapi/linux/kcov_dataflow.h>. In short, every record is
+ *
+ *   [hdr][pc][ptr or cmp type][nvals value words]
+ *
+ * appended after area[0], which counts the record words written so far.
+ */
+#define pr_fmt(fmt) "kcov_dataflow: " fmt
+
+#define DISABLE_BRANCH_PROFILING
+#include <linux/atomic.h>
+#include <linux/bits.h>
+#include <linux/compiler.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/types.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/preempt.h>
+#include <linux/refcount.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/shrinker.h>
+#include <linux/mutex.h>
+#include <linux/hashtable.h>
+#include <linux/vmalloc.h>
+#include <linux/debugfs.h>
+#include <linux/uaccess.h>
+#include <linux/jump_label.h>
+#include <linux/kcov.h>
+#include <uapi/linux/kcov_dataflow.h>
+#include <asm/setup.h>
+
+/*
+ * Comparison capture is shared with mainline kcov; it only exists when both the
+ * trace-cmp instrumentation and the dataflow task state are configured in.
+ */
+#if defined(CONFIG_KCOV_ENABLE_COMPARISONS) && \
+	(defined(CONFIG_KCOV_DATAFLOW_ARGS) || defined(CONFIG_KCOV_DATAFLOW_RET))
+#define KCOV_DF_HAVE_CMP 1
+#endif
+
+#define KCOV_DF_IS_ERR(p)	((unsigned long)(p) >= (unsigned long)-4095UL)
+
+/*
+ * Bit 31 of task_struct::kcov_df_seq is the per-task recursion guard, held
+ * while one of the callbacks below runs. The record sequence number lives in
+ * the low 24 bits (KCOV_DF_HDR_SEQ_MASK) and is advanced with kcov_df_next_seq()
+ * so that it wraps inside its own field and can never carry into the guard.
+ */
+#define KCOV_DF_SEQ_GUARD	BIT(31)
+
+/*
+ * Per-worker private scratch size (u64 words), KCOV's remote-area model: a
+ * remote kworker collects into its OWN scratch and merges it into the shared
+ * ->area at kcov_df_remote_stop(). Fixed and small (8 MiB) -- one work item's
+ * coverage, not a whole buffer -- so the pool of recycled scratch areas stays
+ * bounded regardless of how many kworkers churn. Overflowing a scratch just
+ * drops that worker's excess records (same as a full buffer), never corrupts.
+ */
+#define KCOV_DF_REMOTE_WORDS	(1UL << 20)
+
+struct kcov_dataflow {
+	struct mutex	lock;
+	unsigned int	size;	/* in u64 words */
+	void		*area;
+	/*
+	 * Task with a local (KCOV_DF_ENABLE) session on this object, NULL if
+	 * none. Mirrors struct kcov::t: that task holds its own reference (see
+	 * ->refcount) and points back at us through task_struct::kcov_df, so
+	 * KCOV_DF_DISABLE, close() and task exit all unwire the same session.
+	 */
+	struct task_struct *t;
+	/*
+	 * Lifetime refcount (KCOV's struct kcov pattern). The open fd holds one
+	 * ref; the task enabled with KCOV_DF_ENABLE holds one for as long as its
+	 * session lasts (dropped by KCOV_DF_DISABLE, by close() from that task,
+	 * or by task exit -- it cannot be unwired from another task); each
+	 * kcov_df_remote_start() takes one and the matching kcov_df_remote_stop()
+	 * drops it. Whoever drops the LAST ref frees ->area and the object
+	 * (kcov_df_put), so an instrumented callback can never write through a
+	 * freed buffer, whichever task does the final close().
+	 */
+	refcount_t	refcount;
+	u64		remote_handle; /* handle for remote lookup, 0 if not published */
+#ifdef KCOV_DF_HAVE_CMP
+	/*
+	 * Whether this fd holds a ref on kcov_df_cmp_key, tracked SEPARATELY for
+	 * the local (KCOV_DF_ENABLE) and remote (KCOV_DF_REMOTE_ENABLE) sources.
+	 * A single shared flag let a KCOV_DF_DISABLE drop the key while a remote
+	 * handle was still published -- silently losing the live remote workers'
+	 * comparison records. Two flags mean releasing one source never pulls the
+	 * key out from under the other. Both are only touched under ->lock.
+	 */
+	bool		cmp_key_local;
+	bool		cmp_key_remote;
+#endif
+};
+
+/* Which activation source holds the cmp static key (see kcov_df_cmp_key_hold). */
+enum { KCOV_DF_CMP_LOCAL, KCOV_DF_CMP_REMOTE };
+
+#ifdef KCOV_DF_HAVE_CMP
+/*
+ * Static key gating the per-comparison dataflow check in kcov_trace_cmp()
+ * (linux/kcov.h). It is a patched-out NOP until at least one dataflow session is
+ * live, so trace-cmp across the WHOLE kernel costs nothing extra while no
+ * dataflow fuzzing runs; only an active session flips it on. Refcounted: inc on
+ * each source's first enable, dec on its disable/close/exit (idempotent,
+ * tracked per source via cmp_key_local / cmp_key_remote so releasing one never
+ * drops the key from under the other).
+ *
+ * The key is only ever inc'd/dec'd from ioctl, close() and do_exit() context,
+ * under df->lock -- never from kcov_df_remote_stop() or the last kcov_df_put(),
+ * so a subsystem's worker path never ends up under cpus_read_lock() and
+ * jump_label_mutex. The static_branch_{inc,dec}() text-patch is amortised -- it
+ * fires only on the 0->1 and 1->0 transitions, not per fd while sessions overlap.
+ */
+DEFINE_STATIC_KEY_FALSE(kcov_df_cmp_key);
+EXPORT_SYMBOL(kcov_df_cmp_key);
+
+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which)
+{
+	bool *held = which == KCOV_DF_CMP_LOCAL ? &df->cmp_key_local
+						: &df->cmp_key_remote;
+
+	lockdep_assert_held(&df->lock);
+	if (!*held) {
+		*held = true;
+		static_branch_inc(&kcov_df_cmp_key);
+	}
+}
+
+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which)
+{
+	bool *held = which == KCOV_DF_CMP_LOCAL ? &df->cmp_key_local
+						: &df->cmp_key_remote;
+
+	lockdep_assert_held(&df->lock);
+	if (*held) {
+		*held = false;
+		static_branch_dec(&kcov_df_cmp_key);
+	}
+}
+
+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df)
+{
+	return df->cmp_key_local || df->cmp_key_remote;
+}
+#else
+static void kcov_df_cmp_key_hold(struct kcov_dataflow *df, int which) {}
+static void kcov_df_cmp_key_release(struct kcov_dataflow *df, int which) {}
+static bool kcov_df_cmp_key_held(struct kcov_dataflow *df) { return false; }
+#endif
+
+/* Remote dataflow: handle-based lookup (follows KCOV's kcov_remote_map pattern) */
+static DEFINE_MUTEX(kcov_df_remote_lock);
+static DEFINE_HASHTABLE(kcov_df_remote_map, 4);
+
+struct kcov_df_remote {
+	u64			handle;
+	struct kcov_dataflow	*df;
+	struct hlist_node	hnode;
+};
+
+static struct kcov_df_remote *kcov_df_remote_find(u64 handle)
+{
+	struct kcov_df_remote *remote;
+
+	hash_for_each_possible(kcov_df_remote_map, remote, hnode, handle) {
+		if (remote->handle == handle)
+			return remote;
+	}
+	return NULL;
+}
+
+/* Unpublish @df's remote handle, if any; no new remote session can start. */
+static void kcov_df_remote_unpublish(struct kcov_dataflow *df)
+{
+	struct kcov_df_remote *remote;
+
+	mutex_lock(&kcov_df_remote_lock);
+	if (df->remote_handle) {
+		remote = kcov_df_remote_find(df->remote_handle);
+		if (remote) {
+			hash_del(&remote->hnode);
+			kfree(remote);
+		}
+		df->remote_handle = 0;
+	}
+	mutex_unlock(&kcov_df_remote_lock);
+}
+
+static void kcov_df_get(struct kcov_dataflow *df)
+{
+	refcount_inc(&df->refcount);
+}
+
+/*
+ * Drop a reference; the last one frees the buffer and the object. Only called
+ * from sleepable task context (ioctl, close(), do_exit(), and remote_stop()
+ * which requires it), so vfree() here is fine. No caller may touch @df after
+ * its own kcov_df_put(). Every path that unwires a session releases its cmp
+ * key ref under df->lock first, so nothing is left to balance here.
+ */
+static void kcov_df_put(struct kcov_dataflow *df)
+{
+	if (refcount_dec_and_test(&df->refcount)) {
+		WARN_ON_ONCE(kcov_df_cmp_key_held(df));
+		vfree(df->area);
+		kfree(df);
+	}
+}
+
+/*
+ * Touch every page of a buffer before a task starts collecting into it, the
+ * same way kcov_fault_in_area() does for KCOV_ENABLE: on configurations with
+ * lazily populated vmalloc mappings the first access would otherwise fault
+ * from inside an instrumented callback, and code on the vmalloc fault path may
+ * itself be instrumented.
+ */
+static void kcov_df_fault_in_area(u64 *area, unsigned long size)
+{
+	unsigned long stride = PAGE_SIZE / sizeof(u64);
+	unsigned long off;
+
+	for (off = 0; off < size; off += stride)
+		READ_ONCE(area[off]);
+}
+
+/*
+ * Pool of recycled per-worker scratch areas (KCOV's kcov_remote_areas). All are
+ * KCOV_DF_REMOTE_WORDS u64s. While parked on the freelist the area's first bytes
+ * hold this list_head; while in use word[0] is the scratch write cursor. Guarded
+ * by kcov_df_remote_lock.
+ */
+struct kcov_df_scratch {
+	struct list_head list;
+};
+static LIST_HEAD(kcov_df_scratch_pool);
+static unsigned long kcov_df_scratch_pool_nr;	/* idle areas parked in the pool */
+
+/* Take a scratch area from the pool, or NULL if empty (caller vmalloc()s one). */
+static void *kcov_df_scratch_get(void)
+{
+	struct kcov_df_scratch *s;
+
+	if (list_empty(&kcov_df_scratch_pool))
+		return NULL;
+	s = list_first_entry(&kcov_df_scratch_pool, struct kcov_df_scratch, list);
+	list_del(&s->list);
+	kcov_df_scratch_pool_nr--;
+	return s;
+}
+
+/* Return a scratch area to the pool for reuse. */
+static void kcov_df_scratch_put(void *area)
+{
+	struct kcov_df_scratch *s = area;
+
+	INIT_LIST_HEAD(&s->list);
+	list_add(&s->list, &kcov_df_scratch_pool);
+	kcov_df_scratch_pool_nr++;
+}
+
+/*
+ * Merge a remote worker's private scratch into the shared ->area, appending its
+ * records at the shared write cursor. This is the ONE many-writers path (several
+ * kworkers merge concurrently), so it claims its region with a cmpxchg loop on
+ * area[0]: the bounds are checked against the value about to be committed, and
+ * the commit only happens when the record fits. area[0] therefore never exceeds
+ * the buffer capacity and every counted word has been written, so a consumer
+ * walking area[0] words stays inside its mapping. A concurrent reset by user
+ * space (writing area[0] = 0 to restart collection) simply makes the cmpxchg
+ * fail and the loop re-read the new cursor; there is no subtract, so the
+ * counter can never go negative or wrap past the bounds check. Each merge claims
+ * a disjoint [start, start+n), so concurrent merges don't overlap and need no
+ * lock. @df is kept alive by the caller's reference, so ->area is stable here.
+ *
+ * ->area is never written through kcov_df_reserve() while a remote handle is
+ * published (KCOV_DF_ENABLE refuses that), so this atomic cursor update never
+ * races a plain read-modify-write of the same word.
+ */
+static void kcov_df_merge(struct kcov_dataflow *df, const u64 *scratch)
+{
+	u64 *area = df->area;
+	atomic64_t *cursor;
+	u64 n, count, capacity;
+	s64 old;
+
+	if (!area)
+		return;
+	/*
+	 * scratch[0] is an EXACT high-water of written words: kcov_df_reserve()
+	 * commits the count only after a record fits, so every counted word was
+	 * really written -- the merge never publishes the unwritten
+	 * (recycled/uninitialized) tail of a pooled scratch. The clamp below is thus
+	 * belt-and-suspenders against a stray count.
+	 */
+	n = scratch[0];
+	if (n > KCOV_DF_REMOTE_WORDS - 1)
+		n = KCOV_DF_REMOTE_WORDS - 1;
+	if (!n)
+		return;
+
+	capacity = df->size - 1;	/* words after area[0] */
+	cursor = (atomic64_t *)&area[0];
+	old = atomic64_read(cursor);
+	do {
+		count = old;
+		/* Full (or a garbage cursor from user space): drop the records. */
+		if (count > capacity || n > capacity - count)
+			return;
+	} while (!atomic64_try_cmpxchg(cursor, &old, count + n));
+	memcpy(&area[1 + count], &scratch[1], n * sizeof(u64));
+}
+
+/*
+ * Reserve @record_len u64 words in the current task's buffer. On success return
+ * true and store the 1-based start index of the record's data region.
+ *
+ * Single-writer discipline, identical to mainline kcov.c: the current task is the
+ * ONLY instrumented writer of @area. In remote mode @area is this kworker's OWN
+ * private scratch; in local (KCOV_DF_ENABLE) mode it is the enabling task's own
+ * mmapped buffer -- and only one task can hold that (the KCOV_DF_ENABLE EBUSY
+ * guard, which also refuses a buffer with a published remote handle, so
+ * kcov_df_merge() never touches this word concurrently). Two tasks never write
+ * the same @area here, so no atomic is needed: validate FIRST and commit the
+ * count (area[0]) only on success, so area[0] is always an EXACT high-water of
+ * written words and no consumer (userspace or kcov_df_merge()) ever sees an
+ * unwritten/recycled slot.
+ *
+ * (Publishing a worker's scratch into the shared ->area is the SEPARATE
+ * kcov_df_merge() path, which DOES reserve atomically because many kworkers merge
+ * concurrently.)
+ *
+ * READ_ONCE/WRITE_ONCE because in local mode userspace may reset area[0] to 0
+ * between operations. That reset can only drive the count to 0, never negative
+ * (there is no subtract), so a racing reset may drop records but can never produce
+ * an out-of-bounds store. This is exactly mainline kcov's contract.
+ *
+ * __always_inline because kcov_df_trace_cmp() below is on objtool's
+ * uaccess_safe_builtin[] list, and objtool rejects any out-of-line call made
+ * from such a function; do not leave that to the optimizer.
+ */
+static __always_inline notrace __no_sanitize_coverage bool
+kcov_df_reserve(struct task_struct *t, u64 *area, u32 record_len,
+		unsigned long *start_index)
+{
+	unsigned long count = READ_ONCE(area[0]);
+
+	*start_index = 1 + count;
+	if (count >= t->kcov_df_size ||
+	    record_len > t->kcov_df_size - *start_index)
+		return false;
+	WRITE_ONCE(area[0], count + record_len);
+	return true;
+}
+
+/*
+ * Contexts where dataflow collection must stay completely inert.
+ *
+ * Beyond the obvious !in_task() case, this bails whenever page faults are
+ * disabled. copy_from_kernel_nofault() -- used by kcov_df_write() below to read
+ * traced pointers, and, crucially, by the ORC stack unwinder that KASAN runs on
+ * every slab free (set_track_prepare() -> stack_trace_save()) -- brackets its
+ * raw loads with pagefault_disable(), and those loads carry trace-cmp/trace-args
+ * instrumentation. Without this bail a single stack walk under a fuzzing + KASAN
+ * workload floods the collector with a callback per load and soft-locks the CPU.
+ *
+ * pagefault_disabled() is true throughout any such nofault region no matter
+ * which instrumented leaf issued the callback, so testing it here contains the
+ * whole class of self-instrumentation storms -- the bit-31 recursion guard below
+ * only covers re-entry nested inside our own callback, not a fresh entry from
+ * the unwinder/KASAN path. Contained entirely to this file: no coverage
+ * exclusion in mm/ or arch/ is needed.
+ *
+ * The trade-off is that records are also dropped inside unrelated
+ * pagefault_disable() regions (kmap_atomic() on HIGHMEM, futex and perf
+ * callchain probes, ...). Those are short and rare on the fuzzing workloads this
+ * targets; a per-task "in nofault region" flag would remove the coupling at the
+ * cost of touching mm/maccess.c.
+ */
+static __always_inline notrace __no_sanitize_coverage bool
+kcov_df_inert_context(void)
+{
+	return !in_task() || pagefault_disabled();
+}
+
+/* Same as kcov.c: record PCs with the KASLR offset removed. */
+static __always_inline notrace __no_sanitize_coverage u64
+kcov_df_canonicalize_ip(u64 ip)
+{
+#ifdef CONFIG_RANDOMIZE_BASE
+	ip -= kaslr_offset();
+#endif
+	return ip;
+}
+
+/*
+ * Advance the task's 24-bit record sequence number, keeping the guard bit set.
+ * Masking the increment keeps the counter from ever carrying into
+ * KCOV_DF_SEQ_GUARD, which would reopen re-entry in the middle of a record.
+ */
+static __always_inline notrace __no_sanitize_coverage u32
+kcov_df_next_seq(struct task_struct *t)
+{
+	u32 seq = (t->kcov_df_seq + 1) & KCOV_DF_HDR_SEQ_MASK;
+
+	t->kcov_df_seq = KCOV_DF_SEQ_GUARD | seq;
+	return seq;
+}
+
+static __always_inline notrace __no_sanitize_coverage u64
+kcov_df_hdr(u64 type, u32 nvals, u32 size, u32 arg_idx, u32 seq)
+{
+	return (type << KCOV_DF_HDR_TYPE_SHIFT) |
+	       ((u64)nvals << KCOV_DF_HDR_NVALS_SHIFT) |
+	       ((u64)min_t(u32, size, KCOV_DF_HDR_SIZE_MASK) <<
+		KCOV_DF_HDR_SIZE_SHIFT) |
+	       ((u64)min_t(u32, arg_idx, KCOV_DF_HDR_ARGIDX_MASK) <<
+		KCOV_DF_HDR_ARGIDX_SHIFT) |
+	       (seq & KCOV_DF_HDR_SEQ_MASK);
+}
+
+/*
+ * Core write function for ENTRY/RET records.
+ * Uses the same READ_ONCE/WRITE_ONCE pattern as write_comp_data() in kcov.c.
+ *
+ * @num_fields is the length of the compiler-supplied @offsets table (pairs of
+ * offset,size) for an expanded struct, 0 for a scalar read directly from @ptr
+ * with width @size. It is clamped to KCOV_DF_MAX_VALS so the record length can
+ * never wrap and the field loop is bounded by the words actually reserved.
+ */
+static noinline notrace __no_sanitize_coverage void
+kcov_df_write(u64 type, u64 pc, u32 arg_idx, u32 size, void *ptr,
+	      u64 *offsets, u32 num_fields)
+{
+	struct task_struct *t = current;
+	u64 *area;
+	unsigned long start_index;
+	u32 nvals, seq, i;
+
+	if (kcov_df_inert_context())
+		return;
+
+	if (!t->kcov_df_enabled)
+		return;
+
+	/*
+	 * Prevent recursion: functions called by this callback
+	 * (copy_from_kernel_nofault) may be instrumented. Use the
+	 * sequence counter's high bit as a per-task guard.
+	 */
+	if (t->kcov_df_seq & KCOV_DF_SEQ_GUARD)
+		return;
+	t->kcov_df_seq |= KCOV_DF_SEQ_GUARD;
+	/* Paired with the barrier() before the guard is cleared at out:. */
+	barrier();
+
+	area = (u64 *)t->kcov_df_area;
+	if (!area)
+		goto out;
+
+	if (num_fields > KCOV_DF_MAX_VALS)
+		num_fields = KCOV_DF_MAX_VALS;
+	/* Record: header + pc + ptr, then the fields or one scalar word. */
+	nvals = num_fields > 0 ? num_fields : 1;
+
+	if (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(nvals), &start_index))
+		goto out;
+
+	seq = kcov_df_next_seq(t);
+	area[start_index] = kcov_df_hdr(type, nvals, size, arg_idx, seq);
+	area[start_index + 1] = kcov_df_canonicalize_ip(pc);
+	area[start_index + 2] = (u64)(unsigned long)ptr;
+
+	if (num_fields == 0) {
+		u64 val = 0;
+		u32 sz = size;
+
+		/*
+		 * Read the scalar with a compile-time-constant width for the
+		 * common sizes so the compiler folds away copy_from_kernel_
+		 * nofault()'s runtime size loop and alignment branching; fall
+		 * back to the variable-size byte copy for anything else. A
+		 * faulting read leaves val == 0, matching the prior best-effort
+		 * behaviour.
+		 */
+		if (ptr && !KCOV_DF_IS_ERR(ptr)) {
+			switch (sz) {
+			case 8: {
+				u64 v = 0;
+
+				if (!get_kernel_nofault(v, (u64 *)ptr))
+					val = v;
+				break;
+			}
+			case 4: {
+				u32 v = 0;
+
+				if (!get_kernel_nofault(v, (u32 *)ptr))
+					val = v;
+				break;
+			}
+			case 2: {
+				u16 v = 0;
+
+				if (!get_kernel_nofault(v, (u16 *)ptr))
+					val = v;
+				break;
+			}
+			case 1: {
+				u8 v = 0;
+
+				if (!get_kernel_nofault(v, (u8 *)ptr))
+					val = v;
+				break;
+			}
+			default:
+				if (sz > sizeof(val))
+					sz = sizeof(val);
+				copy_from_kernel_nofault(&val, ptr, sz);
+			}
+		}
+		area[start_index + 3] = val;
+	} else {
+		if (!ptr || KCOV_DF_IS_ERR(ptr)) {
+			for (i = 0; i < num_fields; i++)
+				area[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;
+			goto out;
+		}
+		for (i = 0; i < num_fields; i++) {
+			u64 off, sz, val = KCOV_DF_MAGIC_BAD;
+			void *fa;
+
+			if (copy_from_kernel_nofault(&off, &offsets[i * 2], sizeof(off)) ||
+			    copy_from_kernel_nofault(&sz, &offsets[i * 2 + 1], sizeof(sz))) {
+				area[start_index + 3 + i] = KCOV_DF_MAGIC_BAD;
+				continue;
+			}
+			fa = (void *)((unsigned long)ptr + off);
+			val = 0;
+
+			if (sz <= sizeof(val)) {
+				if (copy_from_kernel_nofault(&val, fa, sz))
+					val = KCOV_DF_MAGIC_BAD;
+			} else {
+				if (copy_from_kernel_nofault(&val, fa, sizeof(val)))
+					val = KCOV_DF_MAGIC_BAD;
+			}
+			area[start_index + 3 + i] = val;
+		}
+	}
+out:
+	/*
+	 * Paired with the barrier() after setting the guard at the top.
+	 * Ensures all record writes are complete before we clear the
+	 * recursion guard.
+	 */
+	barrier();
+	t->kcov_df_seq &= ~KCOV_DF_SEQ_GUARD;
+}
+
+/*
+ * The two compiler-emitted entry points are on objtool's uaccess_safe_builtin[]
+ * list, like the __sanitizer_cov_trace_cmp*() callbacks. The trace-args call is
+ * planted before the terminator of the function's entry block (so that every
+ * spilled value dominates it), not at its first instruction: a function that
+ * opens a user access region and then does an unsafe_get_user() -- an asm goto,
+ * hence a block terminator -- gets the callback AFTER the stac, and objtool
+ * reports "call to __sanitizer_cov_trace_args() with UACCESS enabled".
+ *
+ * objtool validates a listed function with AC set and rejects any out-of-line
+ * call from it, and kcov_df_write() calls copy_from_kernel_nofault(), so bracket
+ * the call with user_access_save()/restore(): that clears AC for the whole
+ * record write (the kasan_report() pattern) and keeps SMAP/PAN protection in
+ * force while the collector runs. It compiles to nothing on architectures
+ * without the feature.
+ */
+#ifdef CONFIG_KCOV_DATAFLOW_ARGS
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,
+			   u64 *offsets, u32 num_fields);
+
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_args(u64 pc, u32 arg_idx, u32 arg_size, void *arg_ptr,
+			   u64 *offsets, u32 num_fields)
+{
+	unsigned long ua_flags = user_access_save();
+
+	kcov_df_write(KCOV_DF_TYPE_ENTRY, pc, arg_idx, arg_size, arg_ptr,
+		      offsets, num_fields);
+	user_access_restore(ua_flags);
+}
+EXPORT_SYMBOL(__sanitizer_cov_trace_args);
+#endif
+
+#ifdef CONFIG_KCOV_DATAFLOW_RET
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,
+			  u64 *offsets, u32 num_fields);
+
+noinline void notrace __no_sanitize_coverage
+__sanitizer_cov_trace_ret(u64 pc, u32 ret_size, void *ret_val,
+			  u64 *offsets, u32 num_fields)
+{
+	unsigned long ua_flags = user_access_save();
+
+	kcov_df_write(KCOV_DF_TYPE_RET, pc, 0, ret_size, ret_val,
+		      offsets, num_fields);
+	user_access_restore(ua_flags);
+}
+EXPORT_SYMBOL(__sanitizer_cov_trace_ret);
+#endif
+
+#ifdef KCOV_DF_HAVE_CMP
+/*
+ * Comparison capture (input-to-state). Reached from the shared
+ * __sanitizer_cov_trace_cmp*() callbacks (kcov.c) via kcov_trace_cmp()
+ * (linux/kcov.h), which fans out to mainline kcov and, when this task has a
+ * dataflow session, here as well, so trace-cmp operand pairs land in the SAME
+ * unified TLV buffer as the arg/ret records. Both operands are recorded, so a
+ * userspace consumer can use them for input-to-state matching, complementing
+ * the arg/ret records.
+ *
+ * Record: [header(CMP|nvals=2|seq)][pc][cmp_type][arg1][arg2].
+ * cmp_type carries KCOV_CMP_SIZE()/KCOV_CMP_CONST bits (see linux/kcov.h) so the
+ * consumer knows operand width and whether one side was a compile-time constant.
+ *
+ * On objtool's uaccess_safe_builtin[] list, so this function makes no
+ * out-of-line call (kcov_df_reserve() and the helpers are __always_inline).
+ */
+noinline notrace __no_sanitize_coverage void
+kcov_df_trace_cmp(u64 cmp_type, u64 arg1, u64 arg2, u64 ip)
+{
+	struct task_struct *t = current;
+	u64 *area;
+	unsigned long start_index;
+	u32 seq;
+
+	if (kcov_df_inert_context())
+		return;
+	if (!t->kcov_df_enabled)
+		return;
+	/* Same recursion guard as kcov_df_write(): bit 31 of the seq counter. */
+	if (t->kcov_df_seq & KCOV_DF_SEQ_GUARD)
+		return;
+	t->kcov_df_seq |= KCOV_DF_SEQ_GUARD;
+	barrier();
+
+	area = (u64 *)t->kcov_df_area;
+	if (!area)
+		goto out;
+
+	/* Single-writer exact-count reservation: see kcov_df_reserve(). */
+	if (!kcov_df_reserve(t, area, KCOV_DF_RECORD_WORDS(2), &start_index))
+		goto out;
+
+	seq = kcov_df_next_seq(t);
+	area[start_index]     = kcov_df_hdr(KCOV_DF_TYPE_CMP, 2, 0, 0, seq);
+	area[start_index + 1] = kcov_df_canonicalize_ip(ip);
+	area[start_index + 2] = cmp_type;
+	area[start_index + 3] = arg1;
+	area[start_index + 4] = arg2;
+out:
+	barrier();
+	t->kcov_df_seq &= ~KCOV_DF_SEQ_GUARD;
+}
+EXPORT_SYMBOL(kcov_df_trace_cmp);
+#endif /* KCOV_DF_HAVE_CMP */
+
+/* Called from kernel/fork.c to clear inherited state. */
+void kcov_dataflow_task_init(struct task_struct *t)
+{
+	t->kcov_df_area = NULL;
+	t->kcov_df_size = 0;
+	t->kcov_df_seq = 0;
+	t->kcov_df_enabled = false;
+	t->kcov_df = NULL;
+	t->kcov_df_remote_depth = 0;
+}
+
+/* Called from kernel/exit.c to tear down the exiting task's session, if any. */
+void kcov_dataflow_task_exit(struct task_struct *t)
+{
+	struct kcov_dataflow *df = t->kcov_df;
+
+	if (!df)
+		return;
+
+	if (t->kcov_df_remote_depth > 0) {
+		/*
+		 * A remote kworker exited between kcov_df_remote_start() and
+		 * _stop() (should not happen -- they bracket a single work item).
+		 * Defensive: drop its partial scratch and release the ref so
+		 * neither the buffer nor the object leaks.
+		 */
+		void *scratch = t->kcov_df_area;
+
+		t->kcov_df_enabled = false;
+		t->kcov_df_area = NULL;
+		t->kcov_df_size = 0;
+		t->kcov_df = NULL;
+		t->kcov_df_remote_depth = 0;
+		vfree(scratch);
+		kcov_df_put(df);
+		return;
+	}
+
+	/*
+	 * Local (KCOV_DF_ENABLE) session on the exiting task. Mirror
+	 * kcov_task_exit(): unwire the task, clear df->t so the object never
+	 * keeps a pointer to a freed task_struct (which a later ioctl or
+	 * close() would compare against current), release the cmp key this
+	 * session held and drop the session's reference.
+	 */
+	t->kcov_df_enabled = false;
+	t->kcov_df_area = NULL;
+	t->kcov_df_size = 0;
+	t->kcov_df = NULL;
+
+	mutex_lock(&df->lock);
+	WARN_ON_ONCE(df->t != t);
+	df->t = NULL;
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);
+	mutex_unlock(&df->lock);
+	kcov_df_put(df);
+}
+
+/* File operations for /sys/kernel/debug/kcov_dataflow */
+
+static int kcov_df_open(struct inode *inode, struct file *filep)
+{
+	struct kcov_dataflow *df;
+
+	df = kzalloc_obj(struct kcov_dataflow, GFP_KERNEL);
+	if (!df)
+		return -ENOMEM;
+	mutex_init(&df->lock);
+	refcount_set(&df->refcount, 1);	/* the open fd's reference */
+	filep->private_data = df;
+	return nonseekable_open(inode, filep);
+}
+
+/*
+ * Unwire the local session that @current holds on @df. Caller holds df->lock
+ * and must drop the session's reference with kcov_df_put() after unlocking.
+ */
+static void kcov_df_disable_local(struct kcov_dataflow *df)
+{
+	lockdep_assert_held(&df->lock);
+	WARN_ON_ONCE(df->t != current || current->kcov_df != df);
+
+	current->kcov_df_enabled = false;
+	current->kcov_df_area = NULL;
+	current->kcov_df_size = 0;
+	current->kcov_df = NULL;
+	df->t = NULL;
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_LOCAL);
+}
+
+static int kcov_df_close(struct inode *inode, struct file *filep)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	bool put_session = false;
+
+	/* Unpublish from remote hash: no new users can start */
+	kcov_df_remote_unpublish(df);
+
+	mutex_lock(&df->lock);
+	kcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);
+	/*
+	 * Only the enabled task can unwire its own session. If another task
+	 * (a sibling thread, a fork()ed child, an SCM_RIGHTS recipient) does
+	 * the final close(), the enabled task keeps its reference and keeps
+	 * collecting until it exits, exactly like mainline kcov.
+	 */
+	if (df->t == current) {
+		kcov_df_disable_local(df);
+		put_session = true;
+	}
+	mutex_unlock(&df->lock);
+
+	if (put_session)
+		kcov_df_put(df);
+	/*
+	 * Drop the fd's reference. If remote workers or the enabled task still
+	 * hold refs, the LAST of them frees ->area via kcov_df_put() -- no drain
+	 * loop, no lost-decrement wedge. The hash entry was already unpublished
+	 * above, so no new remote user can start on this object.
+	 */
+	kcov_df_put(df);
+	return 0;
+}
+
+static int kcov_df_mmap(struct file *filep, struct vm_area_struct *vma)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	unsigned long size, off;
+	struct page *page;
+	void *area;
+	int res = 0;
+
+	mutex_lock(&df->lock);
+	size = df->size * sizeof(u64);
+	if (!df->area || vma->vm_pgoff != 0 ||
+	    vma->vm_end - vma->vm_start != size) {
+		res = -EINVAL;
+		goto out;
+	}
+	area = df->area;
+	mutex_unlock(&df->lock);
+
+	vm_flags_set(vma, VM_DONTEXPAND);
+	for (off = 0; off < size; off += PAGE_SIZE) {
+		page = vmalloc_to_page(area + off);
+		res = vm_insert_page(vma, vma->vm_start + off, page);
+		if (res)
+			return res;
+	}
+	return 0;
+out:
+	mutex_unlock(&df->lock);
+	return res;
+}
+
+static long kcov_df_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
+{
+	struct kcov_dataflow *df = filep->private_data;
+	bool put_session = false;
+	unsigned long size;
+	u64 handle = 0;
+	int res = 0;
+
+	/*
+	 * Fetch the remote handle from user space before taking df->lock.
+	 * get_user() may fault and take mmap_lock, but kcov_df_mmap() takes
+	 * df->lock while holding mmap_lock -- doing the copy under df->lock
+	 * would invert that order and deadlock (reported by lockdep).
+	 */
+	if (cmd == KCOV_DF_REMOTE_ENABLE && get_user(handle, (u64 __user *)arg))
+		return -EFAULT;
+
+	mutex_lock(&df->lock);
+	switch (cmd) {
+	case KCOV_DF_INIT_TRACK:
+		if (df->area) {
+			res = -EBUSY;
+			break;
+		}
+		size = arg;
+		if (size < 2 || size > (128 << 20) / sizeof(u64)) {
+			res = -EINVAL;
+			break;
+		}
+		mutex_unlock(&df->lock);
+		{
+			void *area = vmalloc_user(size * sizeof(u64));
+
+			if (!area)
+				return -ENOMEM;
+			mutex_lock(&df->lock);
+			if (df->area) {
+				mutex_unlock(&df->lock);
+				vfree(area);
+				return -EBUSY;
+			}
+			df->area = area;
+			df->size = size;
+		}
+		break;
+
+	case KCOV_DF_ENABLE:
+		/*
+		 * One writer per buffer: refuse if this object already has a
+		 * local session, if this task already has one (on any fd), or
+		 * if the buffer is (or may still be) a remote merge target -- a
+		 * published handle, or workers still in flight after
+		 * KCOV_DF_REMOTE_DISABLE (any ref beyond the fd's own). The
+		 * local reservation is a plain read-modify-write of area[0]
+		 * that must never race kcov_df_merge()'s atomic one.
+		 */
+		if (!df->area || df->t || df->remote_handle ||
+		    refcount_read(&df->refcount) != 1 || current->kcov_df) {
+			res = -EBUSY;
+			break;
+		}
+		kcov_df_fault_in_area(df->area, df->size);
+		kcov_df_get(df);	/* put in KCOV_DF_DISABLE, close() or task exit */
+		df->t = current;
+		current->kcov_df = df;
+		current->kcov_df_area = df->area;
+		current->kcov_df_size = df->size;
+		current->kcov_df_seq = 0;
+		current->kcov_df_remote_depth = 0;
+		/* Publish the session state before the enable flag. */
+		barrier();
+		current->kcov_df_enabled = true;
+		kcov_df_cmp_key_hold(df, KCOV_DF_CMP_LOCAL);
+		break;
+
+	case KCOV_DF_DISABLE:
+		if (df->t != current) {
+			res = -EINVAL;
+			break;
+		}
+		kcov_df_disable_local(df);
+		put_session = true;
+		break;
+
+	case KCOV_DF_REMOTE_ENABLE: {
+		struct kcov_df_remote *remote;
+
+		if (!df->area ||
+		    !kcov_check_handle(handle, true, true, false)) {
+			res = -EINVAL;
+			break;
+		}
+		/*
+		 * One handle per fd (a second one would leak the first entry
+		 * and leave it pointing at a freed object after close()), and
+		 * never while a local session writes the buffer directly.
+		 */
+		if (df->t || df->remote_handle) {
+			res = -EBUSY;
+			break;
+		}
+		remote = kzalloc_obj(struct kcov_df_remote, GFP_KERNEL);
+		if (!remote) {
+			res = -ENOMEM;
+			break;
+		}
+		remote->handle = handle;
+		remote->df = df;
+		mutex_lock(&kcov_df_remote_lock);
+		if (kcov_df_remote_find(handle)) {
+			mutex_unlock(&kcov_df_remote_lock);
+			kfree(remote);
+			res = -EEXIST;
+			break;
+		}
+		hash_add(kcov_df_remote_map, &remote->hnode, handle);
+		df->remote_handle = handle;
+		mutex_unlock(&kcov_df_remote_lock);
+		kcov_df_cmp_key_hold(df, KCOV_DF_CMP_REMOTE);
+		break;
+	}
+
+	case KCOV_DF_REMOTE_DISABLE:
+		kcov_df_remote_unpublish(df);
+		kcov_df_cmp_key_release(df, KCOV_DF_CMP_REMOTE);
+		break;
+
+	default:
+		res = -ENOTTY;
+	}
+	mutex_unlock(&df->lock);
+
+	if (put_session)
+		kcov_df_put(df);
+	return res;
+}
+
+/* Remote dataflow implementation */
+
+/*
+ * Open a remote dataflow section on this task for @handle. Must be called from
+ * sleepable task context (it takes a mutex and may vmalloc() the scratch); in
+ * softirq/hardirq context it is a no-op, as is the matching stop, so the pair
+ * stays balanced for a call site that brackets a softirq-reachable region.
+ */
+void kcov_df_remote_start(u64 handle)
+{
+	struct kcov_df_remote *remote;
+	struct kcov_dataflow *df;
+	void *scratch;
+
+	/* Dataflow remote coverage is collected in task (kworker) context only. */
+	if (!in_task())
+		return;
+	/*
+	 * A task should only run one session at a time (KCOV's rule). If a
+	 * buggy caller nests inside a remote section, don't re-init and don't
+	 * take a second ref -- just count the depth so the matching inner
+	 * stop() leaves the outer session intact (see kcov_df_remote_stop()).
+	 * Coverage from the nested region is attributed to the outer handle,
+	 * which is safe (no corruption, no early free) even though it is
+	 * imprecise. Inside a local (KCOV_DF_ENABLE) session the depth stays
+	 * 0, so the inner stop() is a no-op and the local session's wiring is
+	 * left untouched; its records simply go to its own buffer.
+	 *
+	 * This check comes first so that every early return below only ever
+	 * happens with no session live -- then the matching stop() has nothing
+	 * to tear down and can never truncate an outer section.
+	 */
+	if (current->kcov_df) {
+		WARN_ON_ONCE(1);
+		if (current->kcov_df_remote_depth > 0 &&
+		    current->kcov_df_remote_depth < INT_MAX)
+			current->kcov_df_remote_depth++;
+		return;
+	}
+	if (!handle)
+		return;
+
+	/* mutex_lock()'s might_sleep() reports an atomic (non-sleepable) caller. */
+	mutex_lock(&kcov_df_remote_lock);
+	remote = kcov_df_remote_find(handle);
+	if (!remote || !remote->df || !remote->df->area) {
+		mutex_unlock(&kcov_df_remote_lock);
+		return;
+	}
+	df = remote->df;
+	kcov_df_get(df);		/* keep @df (and ->area) alive until _stop() */
+	scratch = kcov_df_scratch_get();	/* reuse a pooled scratch if any */
+	mutex_unlock(&kcov_df_remote_lock);
+
+	if (!scratch) {
+		scratch = vmalloc(KCOV_DF_REMOTE_WORDS * sizeof(u64));
+		if (!scratch) {
+			kcov_df_put(df);
+			return;
+		}
+	}
+	((u64 *)scratch)[0] = 0;	/* reset the scratch write cursor */
+	kcov_df_fault_in_area(scratch, KCOV_DF_REMOTE_WORDS);
+
+	/*
+	 * Point this task at its OWN private scratch, NOT df->area. It collects
+	 * here while it runs; kcov_df_remote_stop() merges it into the shared
+	 * buffer. So multiple kworkers on one handle never write the same buffer.
+	 */
+	current->kcov_df_area = scratch;
+	current->kcov_df_size = KCOV_DF_REMOTE_WORDS;
+	current->kcov_df_seq = 0;
+	current->kcov_df = df;		/* pocket it for _stop(); no hash relookup */
+	current->kcov_df_remote_depth = 1;
+	/*
+	 * Publish all session state BEFORE the enable flag (mirrors kcov_start()).
+	 * kcov_df_write() gates on kcov_df_enabled and then reads kcov_df_area, so
+	 * the buffer/handle must be visible first; the barrier keeps the compiler
+	 * from hoisting the enable above them.
+	 */
+	barrier();
+	current->kcov_df_enabled = true;
+}
+EXPORT_SYMBOL_GPL(kcov_df_remote_start);
+
+void kcov_df_remote_stop(void)
+{
+	struct kcov_dataflow *df = current->kcov_df;
+	void *scratch;
+
+	/*
+	 * Same context rule as kcov_df_remote_start(): a stop() in softirq
+	 * context pairs with a start() that was a no-op, and must not touch
+	 * the interrupted task's live session.
+	 */
+	if (!in_task())
+		return;
+	/* No remote session (a local session ignores a stray stop). */
+	if (!df || current->kcov_df_remote_depth == 0)
+		return;
+
+	/*
+	 * Unwind a nested start() (buggy caller): only the OUTERMOST stop tears
+	 * the session down. Inner stops just decrement the depth and return, so
+	 * the buffer/ref survive until the worker is really done with them.
+	 */
+	if (--current->kcov_df_remote_depth > 0)
+		return;
+
+	scratch = current->kcov_df_area;
+
+	/*
+	 * Stop writing FIRST: clear the per-task pointers so this task can no
+	 * longer enter kcov_df_write() / touch the scratch. Then it is safe to
+	 * merge and recycle the scratch and drop the ref.
+	 */
+	current->kcov_df_enabled = false;
+	current->kcov_df_area = NULL;
+	current->kcov_df_size = 0;
+	current->kcov_df = NULL;
+
+	if (scratch) {
+		/*
+		 * Publish this worker's records into the shared buffer,
+		 * then return the scratch to the pool for the next worker.
+		 */
+		kcov_df_merge(df, scratch);
+		mutex_lock(&kcov_df_remote_lock);
+		kcov_df_scratch_put(scratch);
+		mutex_unlock(&kcov_df_remote_lock);
+	}
+
+	/*
+	 * Drop the ref taken in kcov_df_remote_start(). If this is the last one,
+	 * kcov_df_put() frees ->area right here -- safe, because no task writes
+	 * ->area directly anymore (workers write scratch; the merge above is
+	 * done). Dropping via the pocketed @df (not a hash lookup) means an
+	 * already-unpublished entry can never strand the count.
+	 */
+	kcov_df_put(df);
+}
+EXPORT_SYMBOL_GPL(kcov_df_remote_stop);
+
+static const struct file_operations kcov_df_fops = {
+	.open		= kcov_df_open,
+	.unlocked_ioctl	= kcov_df_ioctl,
+	.compat_ioctl	= kcov_df_ioctl,
+	.mmap		= kcov_df_mmap,
+	.release	= kcov_df_close,
+};
+
+/*
+ * Reclaim idle per-worker scratch under memory pressure. The pool otherwise only
+ * ever grows to the peak number of concurrent remote kworkers (each area is 8 MiB)
+ * and is never returned to the allocator; a shrinker lets the VM take the idle
+ * (parked) areas back when it needs the memory. Only pooled areas are freeable;
+ * in-use scratch is not on the list. mutex_trylock keeps the shrinker best-effort
+ * and free of any lock-ordering risk.
+ */
+static unsigned long
+kcov_df_scratch_shrink_count(struct shrinker *sh, struct shrink_control *sc)
+{
+	unsigned long nr;
+
+	if (!mutex_trylock(&kcov_df_remote_lock))
+		return 0;
+	nr = kcov_df_scratch_pool_nr;
+	mutex_unlock(&kcov_df_remote_lock);
+	return nr ? nr : SHRINK_EMPTY;
+}
+
+static unsigned long
+kcov_df_scratch_shrink_scan(struct shrinker *sh, struct shrink_control *sc)
+{
+	struct kcov_df_scratch *s, *tmp;
+	LIST_HEAD(victims);
+	unsigned long freed = 0;
+
+	if (!mutex_trylock(&kcov_df_remote_lock))
+		return SHRINK_STOP;
+	/*
+	 * Detach victims under the lock; free them (each 8 MiB) after unlocking
+	 * so the vfree() latency stays off concurrent remote_start()/stop().
+	 */
+	while (freed < sc->nr_to_scan && !list_empty(&kcov_df_scratch_pool)) {
+		s = list_first_entry(&kcov_df_scratch_pool,
+				     struct kcov_df_scratch, list);
+		list_move(&s->list, &victims);
+		kcov_df_scratch_pool_nr--;
+		freed++;
+	}
+	mutex_unlock(&kcov_df_remote_lock);
+
+	list_for_each_entry_safe(s, tmp, &victims, list)
+		vfree(s);
+	return freed;
+}
+
+static int __init kcov_dataflow_init(void)
+{
+	struct shrinker *shrinker;
+
+	debugfs_create_file_unsafe("kcov_dataflow", 0600, NULL, NULL,
+				   &kcov_df_fops);
+
+	shrinker = shrinker_alloc(0, "kcov-df-scratch");
+	if (shrinker) {
+		shrinker->count_objects = kcov_df_scratch_shrink_count;
+		shrinker->scan_objects = kcov_df_scratch_shrink_scan;
+		shrinker->seeks = DEFAULT_SEEKS;
+		shrinker_register(shrinker);
+	} else {
+		pr_warn("scratch shrinker unavailable, idle remote scratch areas will not be reclaimed\n");
+	}
+	return 0;
+}
+device_initcall(kcov_dataflow_init);
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625e..6b724ae713ce1 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2219,6 +2219,58 @@ config KCOV_SELFTEST
 	  On test failure, causes the kernel to panic. Recommended to be
 	  enabled, ensuring critical functionality works as intended.
 
+config KCOV_DATAFLOW_ARGS
+	bool "Enable KCOV dataflow: function argument capture"
+	depends on KCOV
+	depends on CC_IS_CLANG
+	depends on DEBUG_INFO
+	depends on $(cc-option,-fsanitize-coverage=trace-args)
+	depends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-args)
+	help
+	  Captures function arguments at entry via /sys/kernel/debug/kcov_dataflow.
+	  Struct pointer arguments are auto-expanded using compiler DebugInfo
+	  metadata, recording individual field values at runtime.
+	  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.
+	  Requires clang with -fsanitize-coverage=trace-args support (and,
+	  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus
+	  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under
+	  "Compile-time checks and compiler options" to satisfy DEBUG_INFO.
+
+config KCOV_DATAFLOW_RET
+	bool "Enable KCOV dataflow: return value capture"
+	depends on KCOV
+	depends on CC_IS_CLANG
+	depends on DEBUG_INFO
+	depends on $(cc-option,-fsanitize-coverage=trace-ret)
+	depends on !RUST || $(rustc-option,-Cllvm-args=-sanitizer-coverage-trace-ret)
+	help
+	  Captures function return values via /sys/kernel/debug/kcov_dataflow.
+	  Struct pointer returns are auto-expanded using compiler DebugInfo
+	  metadata, recording individual field values at runtime.
+	  Enable per-module with: KCOV_DATAFLOW_file.o := y in the Makefile.
+	  Requires clang with -fsanitize-coverage=trace-ret support (and,
+	  with CONFIG_RUST, a rustc whose LLVM has the matching pass), plus
+	  debug info: select any CONFIG_DEBUG_INFO_DWARF* option under
+	  "Compile-time checks and compiler options" to satisfy DEBUG_INFO.
+
+config KCOV_DATAFLOW_NO_INLINE
+	bool "Disable inlining for dataflow-instrumented files"
+	depends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET
+	help
+	  Adds -fno-inline to files instrumented with KCOV_DATAFLOW.
+	  This ensures every function boundary is preserved, giving
+	  complete argument visibility. Disable for lower overhead at the
+	  cost of losing argument records for inlined functions.
+
+config KCOV_DATAFLOW_INSTRUMENT_ALL
+	bool "Instrument all kernel code with dataflow coverage"
+	depends on KCOV_DATAFLOW_ARGS || KCOV_DATAFLOW_RET
+	help
+	  Instrument all kernel objects with trace-args/trace-ret
+	  automatically. Individual files or directories can opt out
+	  with KCOV_DATAFLOW_file.o := n or KCOV_DATAFLOW := n.
+	  Warning: significantly increases code size and boot time.
+
 menuconfig RUNTIME_TESTING_MENU
 	bool "Runtime Testing"
 	default y
diff --git a/scripts/Makefile.kcov b/scripts/Makefile.kcov
index 78305a84ba9d2..5fd2aa69d8fd5 100644
--- a/scripts/Makefile.kcov
+++ b/scripts/Makefile.kcov
@@ -9,3 +9,20 @@ kcov-rflags-$(CONFIG_KCOV_ENABLE_COMPARISONS)	+= -Cllvm-args=-sanitizer-coverage
 
 export CFLAGS_KCOV := $(kcov-flags-y)
 export RUSTFLAGS_KCOV := $(kcov-rflags-y)
+
+# KCOV dataflow: trace function args and return values. Each kind is gated by
+# its own Kconfig symbol, matching the #ifdef around the callback it emits calls
+# to in kernel/kcov_dataflow.c (an instrumented object must never reference a
+# callback that is not compiled in). Both variables are empty on a KCOV-only
+# kernel, so a stray per-file KCOV_DATAFLOW_file.o := y is harmless there.
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -fsanitize-coverage=trace-args
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_RET) += -fsanitize-coverage=trace-ret
+kcov-dataflow-flags-$(CONFIG_KCOV_DATAFLOW_NO_INLINE) += -fno-inline
+
+# Rust: only add the trace-args/ret llvm-args (sancov-module pass and level=3
+# are already provided by RUSTFLAGS_KCOV since KCOV_DATAFLOW depends on KCOV).
+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_ARGS) += -Cllvm-args=-sanitizer-coverage-trace-args
+kcov-dataflow-rflags-$(CONFIG_KCOV_DATAFLOW_RET) += -Cllvm-args=-sanitizer-coverage-trace-ret
+
+export CFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-flags-y)
+export RUSTFLAGS_KCOV_DATAFLOW := $(kcov-dataflow-rflags-y)
diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
index 0a4fdd8bd975d..b32fa67ce99af 100644
--- a/scripts/Makefile.lib
+++ b/scripts/Makefile.lib
@@ -88,6 +88,20 @@ _c_flags += $(if $(patsubst n%,, \
 _rust_flags += $(if $(patsubst n%,, \
 	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)$(if $(is-kernel-object),$(CONFIG_KCOV_INSTRUMENT_ALL))), \
 	$(RUSTFLAGS_KCOV))
+# KCOV dataflow. The outer test only honours an explicit KCOV opt-out
+# (KCOV_INSTRUMENT_file.o := n / KCOV_INSTRUMENT := n, the noinstr exclusions):
+# it does not require a KCOV opt-in, so per-file KCOV_DATAFLOW_file.o := y works
+# for modules and out-of-tree objects too. The inner test is the dataflow opt-in:
+# per-file/per-directory, or every kernel object under
+# CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL.
+_c_flags += $(if $(patsubst n%,, \
+	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \
+	$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \
+	$(CFLAGS_KCOV_DATAFLOW)))
+_rust_flags += $(if $(patsubst n%,, \
+	$(KCOV_INSTRUMENT_$(target-stem).o)$(KCOV_INSTRUMENT)y),$(if $(patsubst n%,, \
+	$(KCOV_DATAFLOW_$(target-stem).o)$(KCOV_DATAFLOW)$(if $(is-kernel-object),$(CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL))), \
+	$(RUSTFLAGS_KCOV_DATAFLOW)))
 endif
 
 #
diff --git a/tools/objtool/check.c b/tools/objtool/check.c
index 464f6c9d9ff0b..ae6fe47886395 100644
--- a/tools/objtool/check.c
+++ b/tools/objtool/check.c
@@ -1219,6 +1219,10 @@ static const char *uaccess_safe_builtin[] = {
 	"__tsan_unaligned_write16",
 	/* KCOV */
 	"write_comp_data",
+	/* KCOV dataflow */
+	"kcov_df_trace_cmp",
+	"__sanitizer_cov_trace_args",
+	"__sanitizer_cov_trace_ret",
 	"check_kcov_mode",
 	"__sanitizer_cov_trace_pc",
 	"__sanitizer_cov_trace_const_cmp1",
diff --git a/tools/testing/selftests/kcov_dataflow/.gitignore b/tools/testing/selftests/kcov_dataflow/.gitignore
new file mode 100644
index 0000000000000..4f2957a017957
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+user_ioctl/user_ioctl
+binderfs/binderfs_test
+__pycache__/
diff --git a/tools/testing/selftests/kcov_dataflow/Kbuild b/tools/testing/selftests/kcov_dataflow/Kbuild
new file mode 100644
index 0000000000000..2e19e9008fdca
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Kbuild
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test modules, built as external modules against the configured kernel tree
+# by the selftest Makefile ("make -C $(KDIR) M=$(CURDIR) modules"). Every
+# directory opts its object into dataflow instrumentation with
+# KCOV_DATAFLOW_<object>.o := y, the same per-file switch in-tree code uses.
+obj-m				+= rust_ffi_contract/
+obj-m				+= eight_struct_args_c/
+obj-$(CONFIG_RUST)		+= eight_struct_args_rust/
+obj-$(CONFIG_RUST)		+= rust_kworker_remote/
diff --git a/tools/testing/selftests/kcov_dataflow/Makefile b/tools/testing/selftests/kcov_dataflow/Makefile
new file mode 100644
index 0000000000000..fc979e2d4ecc3
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Makefile
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# kcov_dataflow selftests
+#
+# user_ioctl and binderfs are ordinary kselftest programs. The test modules
+# (one per directory, listed in Kbuild) are built by kbuild against KDIR and
+# are loaded, triggered and checked by test_modules.py; trigger-view.py is
+# the interactive viewer the runner is built on.
+#
+# KDIR is the configured kernel build tree. It defaults to the source tree
+# this directory lives in; point it at the O= directory for out-of-tree
+# builds. Pass the same LLVM=1 CC=clang [RUSTC= RUST_LIB_SRC=] the kernel was
+# built with so that kbuild picks the toolchain that has the trace-args and
+# trace-ret passes.
+KDIR ?= $(abspath ../../../..)
+
+TEST_GEN_PROGS := user_ioctl/user_ioctl binderfs/binderfs_test
+TEST_PROGS := test_modules.py
+TEST_FILES := trigger-view.py
+
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+
+# The .ko files kbuild produces for KDIR's configuration, so that they are
+# built by "all" and copied by "install"; the Rust modules need CONFIG_RUST.
+KMODS := rust_ffi_contract eight_struct_args_c
+ifneq ($(shell grep -s ^CONFIG_RUST=y $(KDIR)/.config),)
+KMODS += eight_struct_args_rust rust_kworker_remote
+endif
+TEST_GEN_FILES := $(foreach m,$(KMODS),$(m)/$(m).ko)
+
+include ../lib.mk
+
+ifneq ($(wildcard $(KDIR)/.config),)
+$(TEST_GEN_FILES): modules
+modules:
+	$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) modules
+clean_modules:
+	$(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) clean
+else
+$(TEST_GEN_FILES):
+	@echo "SKIP $(notdir $@): no configured kernel tree at $(KDIR), set KDIR="
+clean_modules:
+endif
+
+clean: clean_modules
+.PHONY: modules clean_modules
diff --git a/tools/testing/selftests/kcov_dataflow/README.rst b/tools/testing/selftests/kcov_dataflow/README.rst
new file mode 100644
index 0000000000000..1929a357aca47
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/README.rst
@@ -0,0 +1,69 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests
+=======================
+
+Selftests for ``/sys/kernel/debug/kcov_dataflow`` (see
+Documentation/dev-tools/kcov-dataflow.rst).
+
+Layout
+------
+
+Makefile, Kbuild
+    kselftest build: the C programs are built by lib.mk, the test modules
+    (one directory each, listed in Kbuild) by kbuild against ``KDIR``.
+user_ioctl/
+    ioctl interface test (kselftest harness, TAP).
+binderfs/
+    binder ioctls under recording (TAP).
+rust_ffi_contract/, eight_struct_args_c/, eight_struct_args_rust/,
+rust_kworker_remote/
+    test modules; each README.rst says what the module exercises.
+test_modules.py
+    KTAP runner: loads every module, triggers it with recording active and
+    checks the captured arguments, struct fields and return values against
+    the values the module uses. Modules that are not built are SKIPped.
+trigger-view.py
+    Interactive viewer the runner is built on (call tree or ``--raw``
+    records, kallsyms/addr2line symbolization, ``--remote`` capture).
+
+Kernel
+------
+
+The kernel and the modules must be built with a clang that has the
+trace-args/trace-ret passes (and, for the Rust modules, a rustc built
+against that LLVM). The config fragment ``config`` lists what the tests
+need; with virtme-ng::
+
+    vng --build --config tools/testing/selftests/kcov_dataflow/config \
+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+Build
+-----
+
+From the kernel tree, with the same toolchain variables::
+
+    make LLVM=1 headers
+    make -C tools/testing/selftests TARGETS=kcov_dataflow \
+        LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+``KDIR`` defaults to the source tree; pass ``KDIR=<O dir>`` for out-of-tree
+builds. The Rust modules are built only when ``KDIR/.config`` has
+``CONFIG_RUST=y``. ``make ... install INSTALL_PATH=<dir>`` produces a
+self-contained tree with ``run_kselftest.sh``.
+
+Run
+---
+
+On the target (root, debugfs mounted)::
+
+    vng --user root --exec \
+        "tools/testing/selftests/kcov_dataflow/test_modules.py"
+    tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl
+    tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
+
+or, from an installed tree, ``run_kselftest.sh -c kcov_dataflow``.
+``test_modules.py -t <module> -C 8`` runs one module and echoes eight
+records of context around each module record; ``trigger-view.py <module>
+[--raw] [-C N] [--remote] [--vmlinux vmlinux]`` shows the capture
+without checking it.
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/Makefile b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile
new file mode 100644
index 0000000000000..b35de62649924
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+# Standalone build of the binderfs test: make -C tools/testing/selftests/kcov_dataflow/binderfs
+TEST_GEN_PROGS := binderfs_test
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+include ../../lib.mk
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/README.rst b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst
new file mode 100644
index 0000000000000..7fcdce1955c19
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: binderfs
+=================================
+
+Exercises the binder driver via binderfs with kcov_dataflow recording
+active and verifies that argument records are captured at the binder
+ioctl boundaries. Needs CONFIG_ANDROID_BINDERFS=y and binder instrumented
+(``KCOV_DATAFLOW := y`` in drivers/android/Makefile or
+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y); SKIPs without binderfs::
+
+  make -C tools/testing/selftests TARGETS=kcov_dataflow
+  tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
diff --git a/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c
new file mode 100644
index 0000000000000..650798e09b20a
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binderfs selftest for kcov_dataflow
+ *
+ * Exercises the binder driver via binderfs with kcov_dataflow recording
+ * active, then verifies that function argument records were captured at
+ * binder ioctl boundaries.
+ *
+ * Requires: CONFIG_ANDROID_BINDER_IPC=y (or _RUST), CONFIG_ANDROID_BINDERFS=y
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <linux/android/binder.h>
+#include <linux/android/binderfs.h>
+#include <linux/kcov_dataflow.h>
+
+
+#define BUF_SIZE	(1 << 20)
+#define BINDERFS_PATH	"/tmp/binderfs_test"
+#define BINDER_DEV	BINDERFS_PATH "/my_binder"
+
+static int setup_binderfs(void)
+{
+	struct binderfs_device dev = {};
+
+	mkdir(BINDERFS_PATH, 0755);
+
+	if (mount("binder", BINDERFS_PATH, "binder", 0, NULL)) {
+		if (errno == ENODEV || errno == ENOENT) {
+			printf("SKIP: binderfs not available\n");
+			return -1;
+		}
+		perror("mount binderfs");
+		return -1;
+	}
+
+	/* Create a binder device via BINDER_CTL_ADD ioctl */
+	int ctl_fd;
+
+	ctl_fd = open(BINDERFS_PATH "/binder-control", O_RDONLY);
+	if (ctl_fd < 0) {
+		perror("open binder-control");
+		umount(BINDERFS_PATH);
+		return -1;
+	}
+
+	strcpy(dev.name, "my_binder");
+	if (ioctl(ctl_fd, BINDER_CTL_ADD, &dev) && errno != EEXIST) {
+		perror("BINDER_CTL_ADD");
+		close(ctl_fd);
+		umount(BINDERFS_PATH);
+		return -1;
+	}
+	close(ctl_fd);
+	return 0;
+}
+
+static void cleanup_binderfs(void)
+{
+	umount(BINDERFS_PATH);
+	rmdir(BINDERFS_PATH);
+}
+
+int main(void)
+{
+	uint64_t *buf;
+	int df_fd, binder_fd;
+	uint64_t total;
+	int valid = 0;
+
+	printf("TAP version 13\n");
+	printf("1..3\n");
+
+	/* Setup binderfs */
+	if (setup_binderfs()) {
+		printf("ok 1 # SKIP binderfs not available\n");
+		printf("ok 2 # SKIP\n");
+		printf("ok 3 # SKIP\n");
+		return 0;
+	}
+
+	/* Open kcov_dataflow */
+	df_fd = open("/sys/kernel/debug/kcov_dataflow", O_RDWR);
+	if (df_fd < 0) {
+		printf("not ok 1 cannot open kcov_dataflow\n");
+		cleanup_binderfs();
+		return 1;
+	}
+
+	if (ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)) {
+		printf("not ok 1 INIT_TRACK failed\n");
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	buf = mmap(NULL, BUF_SIZE * sizeof(uint64_t),
+		   PROT_READ | PROT_WRITE, MAP_SHARED, df_fd, 0);
+	if (buf == MAP_FAILED) {
+		printf("not ok 1 mmap failed\n");
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	printf("ok 1 kcov_dataflow.binderfs_setup\n");
+
+	/* Open binder device */
+	binder_fd = open(BINDER_DEV, O_RDWR | O_CLOEXEC);
+	if (binder_fd < 0) {
+		printf("not ok 2 cannot open %s: %s\n", BINDER_DEV,
+		       strerror(errno));
+		munmap(buf, BUF_SIZE * sizeof(uint64_t));
+		close(df_fd);
+		cleanup_binderfs();
+		return 1;
+	}
+
+	/* Enable recording and exercise binder ioctls */
+	ioctl(df_fd, KCOV_DF_ENABLE, 0);
+	__atomic_store_n(&buf[0], 0, __ATOMIC_RELAXED);
+
+	/* BINDER_VERSION - simple ioctl that exercises the binder path */
+	struct binder_version ver = {};
+
+	ioctl(binder_fd, BINDER_VERSION, &ver);
+
+	/* BINDER_SET_MAX_THREADS */
+	uint32_t max_threads = 4;
+
+	ioctl(binder_fd, BINDER_SET_MAX_THREADS, &max_threads);
+
+	ioctl(df_fd, KCOV_DF_DISABLE, 0);
+
+	total = __atomic_load_n(&buf[0], __ATOMIC_RELAXED);
+	close(binder_fd);
+
+	if (total > 0)
+		printf("ok 2 kcov_dataflow.binderfs_captured # %lu words\n",
+		       (unsigned long)total);
+	else
+		printf("not ok 2 kcov_dataflow.binderfs_captured # 0 words\n");
+
+	/*
+	 * Walk the records: every header must carry a known type and at least
+	 * one value word, the walk must end exactly at area[0], and at least one
+	 * ENTRY/RET record must come from the binder ioctls (CMP records are
+	 * interleaved with CONFIG_KCOV_ENABLE_COMPARISONS=y).
+	 */
+	if (total <= BUF_SIZE - 1) {
+		uint64_t pos = 1, end = 1 + total;
+		unsigned long nargs = 0;
+
+		while (pos + KCOV_DF_RECORD_HDR_WORDS <= end) {
+			uint64_t hdr = buf[pos];
+			uint32_t type = KCOV_DF_HDR_TYPE(hdr);
+			uint32_t nvals = KCOV_DF_HDR_NVALS(hdr);
+
+			if (nvals < 1 || (type != KCOV_DF_TYPE_ENTRY &&
+					  type != KCOV_DF_TYPE_RET &&
+					  type != KCOV_DF_TYPE_CMP))
+				break;
+			if (type != KCOV_DF_TYPE_CMP)
+				nargs++;
+			pos += KCOV_DF_RECORD_WORDS(nvals);
+		}
+		if (pos == end && nargs > 0)
+			valid = 1;
+		else
+			printf("# walk stopped at word %lu of %lu, %lu ENTRY/RET records\n",
+			       (unsigned long)pos, (unsigned long)end, nargs);
+	}
+
+	if (valid)
+		printf("ok 3 kcov_dataflow.binderfs_valid_records\n");
+	else
+		printf("not ok 3 kcov_dataflow.binderfs_valid_records\n");
+
+	printf("# Totals: pass:%d fail:%d skip:0\n",
+	       valid ? 3 : 2, valid ? 0 : 1);
+
+	munmap(buf, BUF_SIZE * sizeof(uint64_t));
+	close(df_fd);
+	cleanup_binderfs();
+	return valid ? 0 : 1;
+}
diff --git a/tools/testing/selftests/kcov_dataflow/config b/tools/testing/selftests/kcov_dataflow/config
new file mode 100644
index 0000000000000..7f3a2fda0641d
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/config
@@ -0,0 +1,11 @@
+CONFIG_KCOV=y
+CONFIG_KCOV_DATAFLOW_ARGS=y
+CONFIG_KCOV_DATAFLOW_RET=y
+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y
+CONFIG_KCOV_DATAFLOW_NO_INLINE=y
+CONFIG_DEBUG_INFO_DWARF5=y
+CONFIG_DEBUG_FS=y
+CONFIG_MODULES=y
+CONFIG_ANDROID_BINDER_IPC=y
+CONFIG_ANDROID_BINDERFS=y
+CONFIG_RUST=y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile
new file mode 100644
index 0000000000000..04ff83f0a9625
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := eight_struct_args_c.o
+KCOV_DATAFLOW_eight_struct_args_c.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst
new file mode 100644
index 0000000000000..62cddee78cd36
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: eight_struct_args_c
+============================================
+
+C module with 1-8 struct pointer arguments (flat s1..s8), value-nested
+st1..st8 and pointer-linked stp1..stp8 towers (on stack, kmalloc and
+vmalloc), pointer forwarding and a struct return value. Opted in with
+``KCOV_DATAFLOW_eight_struct_args_c.o := y``; test_modules.py checks the
+expanded fields (0x11, 0x22, ...) and every return value::
+
+  ./test_modules.py -t eight_struct_args_c
+  ./trigger-view.py eight_struct_args_c --raw
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c
new file mode 100644
index 0000000000000..c7d06a8e94c38
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_c/eight_struct_args_c.c
@@ -0,0 +1,533 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * eight_struct_args_c.c - Verify kcov_dataflow captures struct pointer
+ * arguments with automatic field expansion.
+ *
+ * Three families of structs are exercised:
+ *
+ *  - Flat structs s1..s8: sN has N u64 members side by side; sf_N takes N
+ *    struct pointer args (s1*..sN*). Tests plain field expansion and multiple
+ *    struct-pointer arguments.
+ *
+ *  - Recursively (value) nested structs st1..st8: stN embeds every smaller
+ *    struct by value, so the nesting deepens with N:
+ *        st1 = { u64 field0 }
+ *        st2 = { u64 field0, st1 field1 }             // { v, {v} }
+ *        stN = { u64 field0, st1 field1, ... st(N-1) field(N-1) }
+ *    The deepest chain in st8 is eight levels deep. Used by the stack tests.
+ *
+ *  - Pointer-linked nested structs stp1..stp8: every member is a POINTER to a
+ *    separately allocated object, so the nesting is followed through the heap:
+ *        stp1 = { u64 *field0 }
+ *        stp2 = { u64 *field0, stp1 *field1 }         // { *v, *{v} }
+ *        stpN = { u64 *field0, stp1 *field1, ... stp(N-1) *field(N-1) }
+ *    Used by the dynamic-allocation (kmalloc/vmalloc) tests.
+ *
+ * Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct to invoke.
+ */
+#include <linux/module.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KCOV dataflow struct field expansion test (flat + nested)");
+
+/* Flat structs: sN has N u64 members. */
+struct s1 { u64 a; };
+struct s2 { u64 a; u64 b; };
+struct s3 { u64 a; u64 b; u64 c; };
+struct s4 { u64 a; u64 b; u64 c; u64 d; };
+struct s5 { u64 a; u64 b; u64 c; u64 d; u64 e; };
+struct s6 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; };
+struct s7 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; };
+struct s8 { u64 a; u64 b; u64 c; u64 d; u64 e; u64 f; u64 g; u64 h; };
+
+/*
+ * Recursively (value) nested structs: stN = { u64 field0; st1 field1; ...;
+ * st(N-1) field(N-1); }. Each stN contains every smaller struct by value, so
+ * the nesting depth grows with N (st8 is eight levels deep along its st7 chain).
+ */
+struct st1 { u64 field0; };
+struct st2 { u64 field0; struct st1 field1; };
+struct st3 { u64 field0; struct st1 field1; struct st2 field2; };
+struct st4 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+};
+struct st5 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+};
+struct st6 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+};
+struct st7 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+	struct st6 field6;
+};
+struct st8 {
+	u64 field0;
+	struct st1 field1;
+	struct st2 field2;
+	struct st3 field3;
+	struct st4 field4;
+	struct st5 field5;
+	struct st6 field6;
+	struct st7 field7;
+};
+
+/*
+ * Pointer-linked nested structs: every member is a POINTER to a separately
+ * allocated object. stpN = { u64 *field0; stp1 *field1; ...; stp(N-1)
+ * *field(N-1); }. The dynamic-allocation tests build one of these per allocator.
+ */
+struct stp1 { u64 *field0; };
+struct stp2 { u64 *field0; struct stp1 *field1; };
+struct stp3 { u64 *field0; struct stp1 *field1; struct stp2 *field2; };
+struct stp4 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+};
+struct stp5 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+};
+struct stp6 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+};
+struct stp7 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+	struct stp6 *field6;
+};
+struct stp8 {
+	u64 *field0;
+	struct stp1 *field1;
+	struct stp2 *field2;
+	struct stp3 *field3;
+	struct stp4 *field4;
+	struct stp5 *field5;
+	struct stp6 *field6;
+	struct stp7 *field7;
+};
+
+/* Prototypes: sf_N takes N struct pointer arguments (s1*, s2*, ..., sN*) */
+u64 sf_1(struct s1 *a);
+u64 sf_2(struct s1 *a, struct s2 *b);
+u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c);
+u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e);
+u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f);
+u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f, struct s7 *g);
+u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d, struct s5 *e,
+	 struct s6 *f, struct s7 *g, struct s8 *h);
+
+/* stf_N takes a pointer to the value-nested stN and sums every reachable field0. */
+u64 stf_1(struct st1 *p);
+u64 stf_2(struct st2 *p);
+u64 stf_3(struct st3 *p);
+u64 stf_4(struct st4 *p);
+u64 stf_5(struct st5 *p);
+u64 stf_6(struct st6 *p);
+u64 stf_7(struct st7 *p);
+u64 stf_8(struct st8 *p);
+
+/* stpf_N follows the pointer-linked stpN and sums every reachable *field0. */
+u64 stpf_1(struct stp1 *p);
+u64 stpf_2(struct stp2 *p);
+u64 stpf_3(struct stp3 *p);
+u64 stpf_4(struct stp4 *p);
+u64 stpf_5(struct stp5 *p);
+u64 stpf_6(struct stp6 *p);
+u64 stpf_7(struct stp7 *p);
+u64 stpf_8(struct stp8 *p);
+
+noinline u64 sf_1(struct s1 *a) { return a->a; }
+EXPORT_SYMBOL(sf_1);
+
+noinline u64 sf_2(struct s1 *a, struct s2 *b) { return a->a + b->b; }
+EXPORT_SYMBOL(sf_2);
+
+noinline u64 sf_3(struct s1 *a, struct s2 *b, struct s3 *c)
+{
+	return a->a + b->b + c->c;
+}
+EXPORT_SYMBOL(sf_3);
+
+noinline u64 sf_4(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return a->a + b->b + c->c + d->d;
+}
+EXPORT_SYMBOL(sf_4);
+
+noinline u64 sf_5(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e)
+{
+	return a->a + b->b + c->c + d->d + e->e;
+}
+EXPORT_SYMBOL(sf_5);
+
+noinline u64 sf_6(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f;
+}
+EXPORT_SYMBOL(sf_6);
+
+noinline u64 sf_7(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f, struct s7 *g)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f + g->g;
+}
+EXPORT_SYMBOL(sf_7);
+
+noinline u64 sf_8(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d,
+		  struct s5 *e, struct s6 *f, struct s7 *g, struct s8 *h)
+{
+	return a->a + b->b + c->c + d->d + e->e + f->f + g->g + h->h;
+}
+EXPORT_SYMBOL(sf_8);
+
+/*
+ * Value-nested functions. Each reads its own field0 and forwards the address of
+ * every nested member into the matching stf_k, so the whole recursive tower is
+ * walked and each nesting level is a distinct instrumented struct-pointer arg.
+ */
+noinline u64 stf_1(struct st1 *p) { return p->field0; }
+EXPORT_SYMBOL(stf_1);
+
+noinline u64 stf_2(struct st2 *p)
+{
+	return p->field0 + stf_1(&p->field1);
+}
+EXPORT_SYMBOL(stf_2);
+
+noinline u64 stf_3(struct st3 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2);
+}
+EXPORT_SYMBOL(stf_3);
+
+noinline u64 stf_4(struct st4 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3);
+}
+EXPORT_SYMBOL(stf_4);
+
+noinline u64 stf_5(struct st5 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4);
+}
+EXPORT_SYMBOL(stf_5);
+
+noinline u64 stf_6(struct st6 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5);
+}
+EXPORT_SYMBOL(stf_6);
+
+noinline u64 stf_7(struct st7 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5) +
+	       stf_6(&p->field6);
+}
+EXPORT_SYMBOL(stf_7);
+
+noinline u64 stf_8(struct st8 *p)
+{
+	return p->field0 + stf_1(&p->field1) + stf_2(&p->field2) +
+	       stf_3(&p->field3) + stf_4(&p->field4) + stf_5(&p->field5) +
+	       stf_6(&p->field6) + stf_7(&p->field7);
+}
+EXPORT_SYMBOL(stf_8);
+
+/*
+ * Pointer-linked functions. Each dereferences its own *field0 and forwards each
+ * (already pointer-typed) nested member into the matching stpf_k, following the
+ * heap-linked tower.
+ */
+noinline u64 stpf_1(struct stp1 *p) { return *p->field0; }
+EXPORT_SYMBOL(stpf_1);
+
+noinline u64 stpf_2(struct stp2 *p)
+{
+	return *p->field0 + stpf_1(p->field1);
+}
+EXPORT_SYMBOL(stpf_2);
+
+noinline u64 stpf_3(struct stp3 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2);
+}
+EXPORT_SYMBOL(stpf_3);
+
+noinline u64 stpf_4(struct stp4 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3);
+}
+EXPORT_SYMBOL(stpf_4);
+
+noinline u64 stpf_5(struct stp5 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4);
+}
+EXPORT_SYMBOL(stpf_5);
+
+noinline u64 stpf_6(struct stp6 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5);
+}
+EXPORT_SYMBOL(stpf_6);
+
+noinline u64 stpf_7(struct stp7 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5) +
+	       stpf_6(p->field6);
+}
+EXPORT_SYMBOL(stpf_7);
+
+noinline u64 stpf_8(struct stp8 *p)
+{
+	return *p->field0 + stpf_1(p->field1) + stpf_2(p->field2) +
+	       stpf_3(p->field3) + stpf_4(p->field4) + stpf_5(p->field5) +
+	       stpf_6(p->field6) + stpf_7(p->field7);
+}
+EXPORT_SYMBOL(stpf_8);
+
+u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d);
+struct s4 sf_ret_struct(struct s1 *a, struct s2 *b);
+
+/* Pointer forwarding: callee receives pointer and passes it to another func */
+noinline u64 sf_fwd_inner(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return a->a + b->b + c->c + d->d;
+}
+EXPORT_SYMBOL(sf_fwd_inner);
+
+noinline u64 sf_fwd(struct s1 *a, struct s2 *b, struct s3 *c, struct s4 *d)
+{
+	return sf_fwd_inner(a, b, c, d);
+}
+EXPORT_SYMBOL(sf_fwd);
+
+/* Struct return value */
+noinline struct s4 sf_ret_struct(struct s1 *a, struct s2 *b)
+{
+	struct s4 ret = { .a = a->a, .b = b->a, .c = b->b, .d = a->a + b->b };
+
+	return ret;
+}
+EXPORT_SYMBOL(sf_ret_struct);
+
+/* Allocator shims so run_stp8() can build the pointer tree with either API. */
+static void *t_kmalloc(size_t n) { return kmalloc(n, GFP_KERNEL); }
+static void *t_vmalloc(size_t n) { return vmalloc(n); }
+static void t_kfree(void *p) { kfree(p); }
+static void t_vfree(void *p) { vfree(p); }
+
+/*
+ * Build the pointer-linked stp8 tower with @alloc (each node separately
+ * allocated), run stpf_8() over it, then free every node with @fr. Sub-nodes
+ * are shared (a DAG); each unique allocation is freed exactly once.
+ */
+static u64 run_stp8(void *(*alloc)(size_t), void (*fr)(void *))
+{
+	u64 ret = 0;
+	u64 *l1 = alloc(sizeof(u64));
+	u64 *l2 = alloc(sizeof(u64));
+	u64 *l3 = alloc(sizeof(u64));
+	u64 *l4 = alloc(sizeof(u64));
+	u64 *l5 = alloc(sizeof(u64));
+	u64 *l6 = alloc(sizeof(u64));
+	u64 *l7 = alloc(sizeof(u64));
+	u64 *l8 = alloc(sizeof(u64));
+	struct stp1 *p1 = alloc(sizeof(*p1));
+	struct stp2 *p2 = alloc(sizeof(*p2));
+	struct stp3 *p3 = alloc(sizeof(*p3));
+	struct stp4 *p4 = alloc(sizeof(*p4));
+	struct stp5 *p5 = alloc(sizeof(*p5));
+	struct stp6 *p6 = alloc(sizeof(*p6));
+	struct stp7 *p7 = alloc(sizeof(*p7));
+	struct stp8 *p8 = alloc(sizeof(*p8));
+
+	if (l1 && l2 && l3 && l4 && l5 && l6 && l7 && l8 &&
+	    p1 && p2 && p3 && p4 && p5 && p6 && p7 && p8) {
+		*l1 = 0x11; *l2 = 0x22; *l3 = 0x33; *l4 = 0x44;
+		*l5 = 0x55; *l6 = 0x66; *l7 = 0x77; *l8 = 0x88;
+
+		p1->field0 = l1;
+		p2->field0 = l2; p2->field1 = p1;
+		p3->field0 = l3; p3->field1 = p1; p3->field2 = p2;
+		p4->field0 = l4; p4->field1 = p1; p4->field2 = p2;
+		p4->field3 = p3;
+		p5->field0 = l5; p5->field1 = p1; p5->field2 = p2;
+		p5->field3 = p3; p5->field4 = p4;
+		p6->field0 = l6; p6->field1 = p1; p6->field2 = p2;
+		p6->field3 = p3; p6->field4 = p4; p6->field5 = p5;
+		p7->field0 = l7; p7->field1 = p1; p7->field2 = p2;
+		p7->field3 = p3; p7->field4 = p4; p7->field5 = p5;
+		p7->field6 = p6;
+		p8->field0 = l8; p8->field1 = p1; p8->field2 = p2;
+		p8->field3 = p3; p8->field4 = p4; p8->field5 = p5;
+		p8->field6 = p6; p8->field7 = p7;
+
+		ret = stpf_8(p8);
+	}
+
+	fr(p8); fr(p7); fr(p6); fr(p5); fr(p4); fr(p3); fr(p2); fr(p1);
+	fr(l8); fr(l7); fr(l6); fr(l5); fr(l4); fr(l3); fr(l2); fr(l1);
+	return ret;
+}
+
+static struct dentry *test_dir;
+
+static ssize_t trigger_write(struct file *f, const char __user *buf,
+			     size_t count, loff_t *ppos)
+{
+	struct s1 v1 = { .a = 0x11 };
+	struct s2 v2 = { .a = 0x11, .b = 0x22 };
+	struct s3 v3 = { .a = 0x11, .b = 0x22, .c = 0x33 };
+	struct s4 v4 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44 };
+	struct s5 v5 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55 };
+	struct s6 v6 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66 };
+	struct s7 v7 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66, .g = 0x77 };
+	struct s8 v8 = { .a = 0x11, .b = 0x22, .c = 0x33, .d = 0x44,
+			 .e = 0x55, .f = 0x66, .g = 0x77, .h = 0x88 };
+
+	/* Recursively (value) nested values: each embeds all the smaller ones. */
+	struct st1 t1 = { .field0 = 0x11 };
+	struct st2 t2 = { .field0 = 0x22, .field1 = t1 };
+	struct st3 t3 = { .field0 = 0x33, .field1 = t1, .field2 = t2 };
+	struct st4 t4 = { .field0 = 0x44, .field1 = t1, .field2 = t2,
+			  .field3 = t3 };
+	struct st5 t5 = { .field0 = 0x55, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4 };
+	struct st6 t6 = { .field0 = 0x66, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4, .field5 = t5 };
+	struct st7 t7 = { .field0 = 0x77, .field1 = t1, .field2 = t2,
+			  .field3 = t3, .field4 = t4, .field5 = t5,
+			  .field6 = t6 };
+	u64 sum = 0;
+
+	/* Flat struct tests: sf_N takes N struct pointer args */
+	sum += sf_1(&v1);
+	sum += sf_2(&v1, &v2);
+	sum += sf_3(&v1, &v2, &v3);
+	sum += sf_4(&v1, &v2, &v3, &v4);
+	sum += sf_5(&v1, &v2, &v3, &v4, &v5);
+	sum += sf_6(&v1, &v2, &v3, &v4, &v5, &v6);
+	sum += sf_7(&v1, &v2, &v3, &v4, &v5, &v6, &v7);
+	sum += sf_8(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8);
+
+	/* Value-nested struct tests (on-stack) */
+	sum += stf_1(&t1);
+	sum += stf_2(&t2);
+	sum += stf_3(&t3);
+	sum += stf_4(&t4);
+	sum += stf_5(&t5);
+	sum += stf_6(&t6);
+	sum += stf_7(&t7);
+	/*
+	 * st8 is 1 KiB; keeping it on the stack alongside t1..t7 blows the 2048-byte
+	 * frame limit (-Wframe-larger-than). Build it on the heap (member-wise, so no
+	 * 1 KiB compound-literal temporary lands on the stack either).
+	 */
+	{
+		struct st8 *t8 = kmalloc(sizeof(*t8), GFP_KERNEL);
+
+		if (t8) {
+			t8->field0 = 0x88;
+			t8->field1 = t1;
+			t8->field2 = t2;
+			t8->field3 = t3;
+			t8->field4 = t4;
+			t8->field5 = t5;
+			t8->field6 = t6;
+			t8->field7 = t7;
+			sum += stf_8(t8);
+			kfree(t8);
+		}
+	}
+
+	/* Dynamic allocation: pointer-linked stp8, each node separately alloc'd */
+	sum += run_stp8(t_kmalloc, t_kfree);	/* heap/slab */
+	sum += run_stp8(t_vmalloc, t_vfree);	/* vmalloc address space */
+
+	/* Pointer forwarding: sf_fwd receives pointers and forwards to inner */
+	sum += sf_fwd(&v1, &v2, &v3, &v4);
+
+	/* Struct return value */
+	{
+		struct s4 ret = sf_ret_struct(&v1, &v2);
+
+		sum += ret.a + ret.b + ret.c + ret.d;
+	}
+
+	/* Keep every call above from being optimised away (sum is otherwise dead). */
+	OPTIMIZER_HIDE_VAR(sum);
+	return count;
+}
+
+static const struct file_operations trigger_fops = {
+	.write = trigger_write,
+};
+
+static int __init eight_struct_args_init(void)
+{
+	test_dir = debugfs_create_dir("kcov_dataflow_test", NULL);
+	debugfs_create_file("trigger_struct", 0200, test_dir, NULL,
+			    &trigger_fops);
+	return 0;
+}
+
+static void __exit eight_struct_args_exit(void)
+{
+	debugfs_remove_recursive(test_dir);
+}
+
+module_init(eight_struct_args_init);
+module_exit(eight_struct_args_exit);
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile
new file mode 100644
index 0000000000000..3017a24774051
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := eight_struct_args_rust.o
+KCOV_DATAFLOW_eight_struct_args_rust.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst
new file mode 100644
index 0000000000000..06e8f8070f6c2
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/README.rst
@@ -0,0 +1,11 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: eight_struct_args_rust
+===============================================
+
+Rust equivalent of eight_struct_args_c (rsf_*, rstf_*, rstpf_* with
+``#[no_mangle]``), built only with CONFIG_RUST=y. Opted in with
+``KCOV_DATAFLOW_eight_struct_args_rust.o := y``::
+
+  ./test_modules.py -t eight_struct_args_rust
+  ./trigger-view.py eight_struct_args_rust --raw
diff --git a/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs
new file mode 100644
index 0000000000000..e5cc3cb87591e
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/eight_struct_args_rust/eight_struct_args_rust.rs
@@ -0,0 +1,646 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Verify kcov_dataflow captures struct pointer arguments with automatic
+//! field expansion for Rust #[repr(C)] structs.
+//!
+//! Rust equivalent of eight_struct_args_c. Two families are exercised:
+//!   - Flat structs S1..S8 (1-8 u64 members) via rsf_N.
+//!   - Recursively (value) nested structs St1..St8, where StN embeds every
+//!     smaller struct by value:
+//!         St1 = { field0 }
+//!         St2 = { field0, field1: St1 }              // { v, {v} }
+//!         StN = { field0, field1: St1, ..., field(N-1): St(N-1) }
+//!     so St8 is eight levels deep along its St7 chain. Each rstf_N reads its
+//!     own field0 and forwards each nested member's address into rstf_k.
+//!   - Pointer-linked nested structs Stp1..Stp8, where every member is a raw
+//!     pointer to a separately allocated object:
+//!         Stp1 = { field0: *const u64 }
+//!         StpN = { field0: *const u64, field1: *const Stp1, ... }
+//!     The heap (KBox) test builds this tower and follows it via rstpf_N.
+//!
+//! Write to /sys/kernel/debug/kcov_dataflow_test/trigger_struct_rust to invoke.
+
+#![allow(missing_docs)]
+
+use kernel::prelude::*;
+use kernel::alloc::KBox;
+use kernel::c_str;
+
+module !{
+	type:EightStructArgsRust,
+	name: "eight_struct_args_rust",
+	authors: ["kcov-dataflow"],
+	description: "Struct field expansion test for kcov_dataflow (Rust)",
+	license: "GPL",
+}
+#[repr(C)]
+pub struct S1 {
+	pub a : u64
+}
+#[repr(C)]
+pub struct S2 {
+	pub a : u64, pub b : u64
+}
+#[repr(C)]
+pub struct S3 {
+	pub a : u64, pub b : u64, pub c : u64
+}
+#[repr(C)]
+pub struct S4 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64
+}
+#[repr(C)]
+pub struct S5 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64
+}
+#[repr(C)]
+pub struct S6 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64
+}
+#[repr(C)]
+pub struct S7 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64, pub g : u64
+}
+#[repr(C)]
+pub struct S8 {
+	pub a : u64, pub b : u64, pub c : u64, pub d : u64, pub e : u64,
+		pub f : u64, pub g : u64, pub h : u64
+}
+// Recursively nested: StN = { field0, field1: St1, ..., field(N-1): St(N-1) }.
+// Copy so a smaller value can be embedded into every larger one.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St1 {
+	pub field0 : u64
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St2 {
+	pub field0 : u64, pub field1 : St1
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St3 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St4 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St5 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St6 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St7 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5, pub field6 : St6
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct St8 {
+	pub field0 : u64, pub field1 : St1, pub field2 : St2, pub field3 : St3,
+		pub field4 : St4, pub field5 : St5, pub field6 : St6,
+		pub field7 : St7
+}
+// Pointer-linked nested: every member is a raw pointer to a separately
+// allocated object. StpN = { field0: *const u64, field1: *const Stp1, ... }.
+#[repr(C)]
+pub struct Stp1 {
+	pub field0 : *const u64
+}
+#[repr(C)]
+pub struct Stp2 {
+	pub field0 : *const u64, pub field1 : *const Stp1
+}
+#[repr(C)]
+pub struct Stp3 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2
+}
+#[repr(C)]
+pub struct Stp4 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3
+}
+#[repr(C)]
+pub struct Stp5 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4
+}
+#[repr(C)]
+pub struct Stp6 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5
+}
+#[repr(C)]
+pub struct Stp7 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5,
+		pub field6 : *const Stp6
+}
+#[repr(C)]
+pub struct Stp8 {
+	pub field0 : *const u64, pub field1 : *const Stp1,
+		pub field2 : *const Stp2, pub field3 : *const Stp3,
+		pub field4 : *const Stp4, pub field5 : *const Stp5,
+		pub field6 : *const Stp6, pub field7 : *const Stp7
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_1(a : *const S1) -> u64
+{
+	unsafe
+	{
+		(*a).a
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_2(a : *const S1, b : *const S2) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_4(a : *const S1, b : *const S2, c : *const S3,
+			d : *const S4) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_8(a : *const S1, b : *const S2, c : *const S3,
+			d : *const S4, e : *const S5, f : *const S6,
+			g : *const S7, h : *const S8) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d + (*e).e + (*f).f + (*g).g +
+			(*h).h
+	}
+}
+
+// Recursively nested: each reads its own field0 and forwards every nested
+// member's address into the matching rstf_k, walking the whole tower.
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_1(p : *const St1) -> u64
+{
+	unsafe
+	{
+		(*p).field0
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_2(p : *const St2) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_3(p : *const St3) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_4(p : *const St4) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_5(p : *const St5) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_6(p : *const St6) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_7(p : *const St7) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5) + rstf_6(&(*p).field6)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstf_8(p : *const St8) -> u64
+{
+	unsafe
+	{
+		(*p).field0 + rstf_1(&(*p).field1) + rstf_2(&(*p).field2) +
+			rstf_3(&(*p).field3) + rstf_4(&(*p).field4) +
+			rstf_5(&(*p).field5) + rstf_6(&(*p).field6) +
+			rstf_7(&(*p).field7)
+	}
+}
+
+// Pointer-linked: each dereferences its own *field0 and forwards each
+// (already pointer-typed) nested member into the matching rstpf_k.
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_1(p : *const Stp1) -> u64
+{
+	unsafe
+	{
+		*(*p).field0
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_2(p : *const Stp2) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_3(p : *const Stp3) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_4(p : *const Stp4) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_5(p : *const Stp5) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_6(p : *const Stp6) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_7(p : *const Stp7) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5) + rstpf_6((*p).field6)
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rstpf_8(p : *const Stp8) -> u64
+{
+	unsafe
+	{
+		*(*p).field0 + rstpf_1((*p).field1) + rstpf_2((*p).field2) +
+			rstpf_3((*p).field3) + rstpf_4((*p).field4) +
+			rstpf_5((*p).field5) + rstpf_6((*p).field6) +
+			rstpf_7((*p).field7)
+	}
+}
+
+// Build the pointer-linked Stp8 tower with KBox (each node its own allocation),
+// run rstpf_8 over it, and return the sum. The KBoxes own the storage and hold
+// raw pointers into their siblings; everything is freed when they drop at the
+// end of this function. `?` frees any already-allocated KBoxes on OOM.
+fn build_and_run_stp8() -> Result<u64>
+{
+	let l1 = KBox::new (0x11u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l2 = KBox::new (0x22u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l3 = KBox::new (0x33u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l4 = KBox::new (0x44u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l5 = KBox::new (0x55u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l6 = KBox::new (0x66u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l7 = KBox::new (0x77u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+	let l8 = KBox::new (0x88u64, kernel::alloc::flags::GFP_KERNEL) ? ;
+
+	let p1 = KBox::new (Stp1{ field0: &*l1 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p2 = KBox::new (Stp2{ field0: &*l2, field1: &*p1 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p3 = KBox::new (Stp3{ field0: &*l3, field1: &*p1, field2: &*p2 },
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p4 = KBox::new (
+		Stp4{ field0: &*l4, field1: &*p1, field2: &*p2, field3: &*p3 },
+		kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p5 = KBox::new (Stp5{
+		field0: &*l5,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p6 = KBox::new (Stp6{
+		field0: &*l6,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p7 = KBox::new (Stp7{
+		field0: &*l7,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5,
+		field6: &*p6
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+	let p8 = KBox::new (Stp8{
+		field0: &*l8,
+		field1: &*p1,
+		field2: &*p2,
+		field3: &*p3,
+		field4: &*p4,
+		field5: &*p5,
+		field6: &*p6,
+		field7: &*p7
+	},
+			    kernel::alloc::flags::GFP_KERNEL) ?
+		;
+
+	Ok(rstpf_8(&*p8))
+}
+
+/* Pointer forwarding: receives pointers and passes to inner */
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_fwd_inner(a : *const S1, b : *const S2, c : *const S3,
+				d : *const S4) -> u64
+{
+	unsafe
+	{
+		(*a).a + (*b).b + (*c).c + (*d).d
+	}
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_fwd(a : *const S1, b : *const S2, c : *const S3,
+			  d : *const S4) -> u64{ rsf_fwd_inner(a, b, c, d) }
+
+/* Struct return value */
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn rsf_ret_struct(a : *const S1, b : *const S2)
+	->S4
+{
+	unsafe
+	{
+		S4
+		{
+a:
+			(*a).a, b : (*b).a, c : (*b).b, d : (*a).a + (*b).b
+		}
+	}
+}
+
+unsafe extern "C" fn write_handler(_file : *mut kernel::bindings::file,
+				   _buf : *const core::ffi::c_char,
+				   count : usize,
+				   _ppos : *mut kernel::bindings::loff_t, )
+	-> kernel::ffi::c_long
+{
+	let v1 = S1{ a: 0x11 };
+	let v2 = S2{ a: 0x11, b: 0x22 };
+	let v3 = S3{ a: 0x11, b: 0x22, c: 0x33 };
+	let v4 = S4{ a: 0x11, b: 0x22, c: 0x33, d: 0x44 };
+	let v5 = S5{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55 };
+	let v6 = S6{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66 };
+	let v7 =
+	S7{ a: 0x11, b: 0x22, c: 0x33, d: 0x44, e: 0x55, f: 0x66, g: 0x77 };
+	let v8 = S8{
+		a: 0x11,
+		b: 0x22,
+		c: 0x33,
+		d: 0x44,
+		e: 0x55,
+		f: 0x66,
+		g: 0x77,
+		h: 0x88
+	};
+
+	// Recursively nested values: each embeds all the smaller ones (Copy).
+	let t1 = St1{ field0: 0x11 };
+	let t2 = St2{ field0: 0x22, field1: t1 };
+	let t3 = St3{ field0: 0x33, field1: t1, field2: t2 };
+	let t4 = St4{ field0: 0x44, field1: t1, field2: t2, field3: t3 };
+	let t5 =
+	St5{ field0: 0x55, field1: t1, field2: t2, field3: t3, field4: t4 };
+	let t6 = St6{
+		field0: 0x66,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5
+	};
+	let t7 = St7{
+		field0: 0x77,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5,
+		field6: t6
+	};
+	let t8 = St8{
+		field0: 0x88,
+		field1: t1,
+		field2: t2,
+		field3: t3,
+		field4: t4,
+		field5: t5,
+		field6: t6,
+		field7: t7
+	};
+
+	let mut sum : u64 = 0;
+	sum = sum.wrapping_add(rsf_1(&v1 as *const S1));
+	sum = sum.wrapping_add(rsf_2(&v1 as *const S1, &v2 as *const S2));
+	sum = sum.wrapping_add(rsf_4(&v1 as *const S1, &v2 as *const S2,
+				     &v3 as *const S3, &v4 as *const S4));
+	sum = sum.wrapping_add(rsf_8(&v1 as *const S1, &v2 as *const S2,
+				     &v3 as *const S3, &v4 as *const S4,
+				     &v5 as *const S5, &v6 as *const S6,
+				     &v7 as *const S7, &v8 as *const S8));
+
+	// Recursively nested struct tests
+	sum = sum.wrapping_add(rstf_1(&t1 as *const St1));
+	sum = sum.wrapping_add(rstf_2(&t2 as *const St2));
+	sum = sum.wrapping_add(rstf_3(&t3 as *const St3));
+	sum = sum.wrapping_add(rstf_4(&t4 as *const St4));
+	sum = sum.wrapping_add(rstf_5(&t5 as *const St5));
+	sum = sum.wrapping_add(rstf_6(&t6 as *const St6));
+	sum = sum.wrapping_add(rstf_7(&t7 as *const St7));
+	sum = sum.wrapping_add(rstf_8(&t8 as *const St8));
+
+	// Pointer forwarding: rsf_fwd receives and passes to rsf_fwd_inner
+	sum = sum.wrapping_add(rsf_fwd(&v1 as *const S1, &v2 as *const S2,
+				       &v3 as *const S3, &v4 as *const S4));
+
+	// Struct return value
+	let ret = rsf_ret_struct(&v1 as *const S1, &v2 as *const S2);
+	sum = sum.wrapping_add(ret.a + ret.b + ret.c + ret.d);
+
+	// Dynamic allocation: pointer-linked Stp8 tower (each node its own KBox)
+	if let
+		Ok(s) = build_and_run_stp8()
+		{
+			sum = sum.wrapping_add(s);
+		}
+
+	core::hint::black_box(sum);
+	count as kernel::ffi::c_long
+}
+
+#[repr(transparent)]
+struct SyncFops(kernel::bindings::file_operations);
+unsafe impl Sync for SyncFops
+{
+}
+
+static FOPS : SyncFops = SyncFops(kernel::bindings::file_operations{
+	write: Some(unsafe{ core::mem::transmute(write_handler as *const()) }),
+	..unsafe{ core::mem::zeroed() }
+});
+
+struct EightStructArgsRust {
+	dir : *mut kernel::bindings::dentry,
+}
+
+impl kernel::Module for EightStructArgsRust
+{
+    fn init(_module: &'static ThisModule) -> Result<Self> {
+        let dir = unsafe {
+            kernel::bindings::debugfs_create_dir(
+                c_str!("kcov_dataflow_test").as_char_ptr(),
+                core::ptr::null_mut(),
+            )
+        };
+        unsafe {
+            kernel::bindings::debugfs_create_file_unsafe(
+                c_str!("trigger_struct_rust").as_char_ptr(),
+                0o222,
+                dir,
+                core::ptr::null_mut(),
+                &FOPS.0,
+            )
+        };
+        Ok(Self { dir })
+}
+}
+
+impl Drop for EightStructArgsRust
+{
+	fn drop(&mut self)
+	{
+		unsafe{ kernel::bindings::debugfs_remove(self.dir) };
+	}
+}
+
+unsafe impl Send for EightStructArgsRust
+{
+}
+unsafe impl Sync for EightStructArgsRust
+{
+}
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile
new file mode 100644
index 0000000000000..d2a0261070b1c
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := rust_ffi_contract.o
+KCOV_DATAFLOW_rust_ffi_contract.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst
new file mode 100644
index 0000000000000..291621fa799cd
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: rust_ffi_contract
+==========================================
+
+FFI contract violation detection: ffi_alloc_buf() returns 0 but leaves
+alloc->buffer NULL, and ffi_check_result() receives that NULL. The test
+checks the expanded ``struct ffi_alloc`` at both boundaries, the scalar
+arguments (256, 16, 1), the 0 return and the -EFAULT from the checker.
+Opted in with ``KCOV_DATAFLOW_rust_ffi_contract.o := y``::
+
+  ./test_modules.py -t rust_ffi_contract
+  ./trigger-view.py rust_ffi_contract -C 8
diff --git a/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
new file mode 100644
index 0000000000000..071bd25dfec11
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * rust_ffi_contract.c - Demonstrates kcov_dataflow detecting an FFI
+ * contract violation at a function boundary.
+ *
+ * The pattern: caller passes a struct pointer to callee. Callee's
+ * contract says "returns 0 implies out->buffer is valid". A bug in
+ * the async path returns 0 but leaves buffer=NULL.
+ *
+ * kcov_dataflow captures:
+ *   [ENTRY] ffi_alloc_buf(alloc={.buffer=NULL, .data_size=0}, 256, 16, 1)
+ *   [RET]   ffi_alloc_buf() = 0
+ *   [ENTRY] ffi_check_result(alloc={.buffer=NULL, .data_size=0x110, ...})
+ *                             ^ proves contract violated
+ *   [RET]   ffi_check_result() = -EFAULT
+ *
+ * Write to /sys/kernel/debug/kcov_dataflow_test/rust_ffi_trigger to run.
+ */
+#include <linux/module.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("FFI contract violation detection via kcov_dataflow");
+
+struct ffi_alloc {
+	void *buffer;
+	u64 data_size;
+	u32 free_async;
+	u32 flags;
+};
+
+/* Prototypes */
+int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,
+		  u64 offsets_size, int is_async);
+int ffi_check_result(struct ffi_alloc *alloc);
+
+/*
+ * Callee with contract: returns 0 implies alloc->buffer is valid.
+ * BUG: async path with free_async==0 returns 0 but buffer stays NULL.
+ */
+noinline int ffi_alloc_buf(struct ffi_alloc *alloc, u64 data_size,
+			   u64 offsets_size, int is_async)
+{
+	/*
+	 * data_size + offsets_size is used on every path so that the compiler
+	 * keeps offsets_size alive (an unused parameter is dropped at -O2 and
+	 * callers then pass poison, leaving nothing to trace).
+	 */
+	if (!is_async) {
+		alloc->buffer = kmalloc(data_size + offsets_size, GFP_KERNEL);
+		if (!alloc->buffer)
+			return -ENOMEM;
+		return 0;
+	}
+	/* BUG: returns success but buffer is NULL when pool empty */
+	if (alloc->free_async == 0) {
+		alloc->buffer = NULL;
+		alloc->data_size = data_size + offsets_size;
+		return 0; /* contract violation */
+	}
+	alloc->buffer = kmalloc(data_size + offsets_size, GFP_KERNEL);
+	alloc->free_async--;
+	return 0;
+}
+EXPORT_SYMBOL(ffi_alloc_buf);
+
+/* Caller that trusts the contract */
+noinline int ffi_check_result(struct ffi_alloc *alloc)
+{
+	if (!alloc->buffer) {
+		pr_err("ffi_contract: VIOLATION detected - buffer is NULL after success\n");
+		return -EFAULT;
+	}
+	kfree(alloc->buffer);
+	return 0;
+}
+EXPORT_SYMBOL(ffi_check_result);
+
+static struct dentry *test_dir;
+
+static ssize_t rust_ffi_trigger_write(struct file *f, const char __user *buf,
+				 size_t count, loff_t *ppos)
+{
+	struct ffi_alloc alloc = { .buffer = NULL, .data_size = 0,
+				   .free_async = 0, .flags = 0 };
+	int ret;
+
+	/*
+	 * Keep the initializer: the callee provably writes alloc->buffer before
+	 * reading it, so without the barrier the compiler drops the NULL store
+	 * and the ENTRY record would show stack garbage instead of NULL.
+	 */
+	barrier_data(&alloc);
+
+	/* Trigger the bug: is_async=1, free_async=0 */
+	ret = ffi_alloc_buf(&alloc, 256, 16, 1);
+	pr_info("ffi_contract: ffi_alloc_buf returned %d, buffer=%p\n",
+		ret, alloc.buffer);
+
+	if (ret == 0)
+		ffi_check_result(&alloc);
+
+	return count;
+}
+
+static const struct file_operations rust_ffi_trigger_fops = {
+	.write = rust_ffi_trigger_write,
+};
+
+static int __init ffi_contract_init(void)
+{
+	test_dir = debugfs_create_dir("kcov_dataflow_test", NULL);
+	debugfs_create_file("rust_ffi_trigger", 0200, test_dir, NULL,
+			    &rust_ffi_trigger_fops);
+	return 0;
+}
+
+static void __exit ffi_contract_exit(void)
+{
+	debugfs_remove_recursive(test_dir);
+}
+
+module_init(ffi_contract_init);
+module_exit(ffi_contract_exit);
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile
new file mode 100644
index 0000000000000..cb7392a50b1a9
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-m := rust_kworker_remote.o
+KCOV_DATAFLOW_rust_kworker_remote.o := y
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst
new file mode 100644
index 0000000000000..aff597ab67aea
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/README.rst
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests: rust_kworker_remote
+============================================
+
+Rust module testing kcov_df_remote_start()/kcov_df_remote_stop() from
+kworker context: the trigger queues a work item on system_wq whose three
+phases (populate/update/drain of a CompositeStore of RBTrees) run with
+remote capture on handle 1, which the runner publishes with
+KCOV_DF_REMOTE_ENABLE. Built only with CONFIG_RUST=y::
+
+  ./test_modules.py -t rust_kworker_remote
+  ./trigger-view.py rust_kworker_remote --remote
diff --git a/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs
new file mode 100644
index 0000000000000..65c5722c383cc
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_kworker_remote/rust_kworker_remote.rs
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Test kcov_df_remote_start/stop from kworker context.
+//!
+//! A composite struct holds three RBTrees (simulating RBTree/XArray/maple_tree
+//! workloads). Three work phases run on system_wq:
+//!   Phase 1 (populate): fill all three trees
+//!   Phase 2 (update): insert new values, read existing, overwrite
+//!   Phase 3 (drain): remove all entries
+//!
+//! User space publishes a buffer with KCOV_DF_REMOTE_ENABLE, writes to
+//! /sys/kernel/debug/kcov_dataflow_test/trigger_kworker_remote, then reads
+//! the captured records.
+
+#![allow(missing_docs)]
+
+use kernel::prelude::*;
+use kernel::sync::{Arc, Completion};
+use kernel::workqueue::{self, impl_has_work, new_work, Work, WorkItem};
+use kernel::rbtree::RBTree;
+use kernel::c_str;
+
+module! {
+    type: RustKworkerRemote,
+    name: "rust_kworker_remote",
+    authors: ["kcov-dataflow"],
+    description: "Test kcov_df_remote capturing from kworker (RBTree composite)",
+    license: "GPL",
+}
+
+// Extern bindings for kcov_dataflow remote API (kernel/kcov_dataflow.c)
+unsafe extern "C" {
+    fn kcov_df_remote_start(handle: u64);
+    fn kcov_df_remote_stop();
+}
+
+/// Composite data structure: three trees with different key ranges.
+/// Simulates a real driver managing multiple lookup tables.
+struct CompositeStore {
+    /// Primary index (keys 0..N)
+    primary: RBTree<u64, u64>,
+    /// Secondary/auxiliary index (keys 100..N)
+    aux: RBTree<u64, u64>,
+    /// Scratch/temp space (keys 200..N)
+    scratch: RBTree<u64, u64>,
+}
+
+impl CompositeStore {
+    fn new() -> Self {
+        Self {
+            primary: RBTree::new(),
+            aux: RBTree::new(),
+            scratch: RBTree::new(),
+        }
+    }
+
+    /// Phase 1: populate all three trees with initial data.
+    #[inline(never)]
+    fn populate(&mut self) -> Result {
+        for i in 0u64..8 {
+            self.primary.try_create_and_insert(i, i * 0x1111, GFP_KERNEL)?;
+        }
+        for i in 100u64..108 {
+            self.aux.try_create_and_insert(i, i * 0x2222, GFP_KERNEL)?;
+        }
+        for i in 200u64..208 {
+            self.scratch.try_create_and_insert(i, i * 0x3333, GFP_KERNEL)?;
+        }
+        Ok(())
+    }
+
+    /// Phase 2: insert more, read existing, overwrite some.
+    #[inline(never)]
+    fn update(&mut self) -> Result {
+        // Insert new entries into primary
+        for i in 8u64..12 {
+            self.primary.try_create_and_insert(i, i * 0x4444, GFP_KERNEL)?;
+        }
+        // Read from aux (get passes &K which is a struct arg)
+        for i in 100u64..108 {
+            let _ = self.aux.get(&i);
+        }
+        // Overwrite scratch entries
+        for i in 200u64..204 {
+            self.scratch.remove(&i);
+            self.scratch.try_create_and_insert(i, i * 0x5555, GFP_KERNEL)?;
+        }
+        Ok(())
+    }
+
+    /// Phase 3: drain all trees.
+    #[inline(never)]
+    fn drain(&mut self) {
+        while let Some(c) = self.primary.cursor_front_mut() {
+            c.remove_current();
+        }
+        while let Some(c) = self.aux.cursor_front_mut() {
+            c.remove_current();
+        }
+        while let Some(c) = self.scratch.cursor_front_mut() {
+            c.remove_current();
+        }
+    }
+}
+
+/// Work item that runs three phases in kworker context with remote capture.
+#[pin_data]
+struct RemoteWork {
+    #[pin]
+    work: Work<RemoteWork>,
+    #[pin]
+    done: Completion,
+}
+
+impl_has_work! {
+    impl HasWork<Self> for RemoteWork { self.work }
+}
+
+impl RemoteWork {
+    fn new() -> Result<Arc<Self>> {
+        Arc::pin_init(pin_init!(RemoteWork {
+            work <- new_work!("RemoteWork::work"),
+            done <- Completion::new(),
+        }), GFP_KERNEL)
+    }
+}
+
+impl WorkItem for RemoteWork {
+    type Pointer = Arc<RemoteWork>;
+
+    fn run(this: Arc<RemoteWork>) {
+        // Enable remote kcov_dataflow capture for this kworker task.
+        // SAFETY: FFI call to exported kernel symbol; no-op if no buffer published.
+        // Handle 1 matches what trigger-view.py passes via KCOV_DF_REMOTE_ENABLE.
+        unsafe { kcov_df_remote_start(1) };
+
+        let mut store = CompositeStore::new();
+        let _ = store.populate();
+        let _ = store.update();
+        store.drain();
+
+        // SAFETY: FFI call to exported kernel symbol; disables capture.
+        unsafe { kcov_df_remote_stop() };
+
+        this.done.complete_all();
+    }
+}
+
+// --- Debugfs trigger (same raw pattern as eight_struct_args_rust) ---
+
+unsafe extern "C" fn write_handler(
+    _file: *mut kernel::bindings::file,
+    _buf: *const core::ffi::c_char,
+    count: usize,
+    _ppos: *mut kernel::bindings::loff_t,
+) -> kernel::ffi::c_long {
+    let work = match RemoteWork::new() {
+        Ok(w) => w,
+        Err(_) => return -(kernel::bindings::ENOMEM as kernel::ffi::c_long),
+    };
+    let waiter = work.clone();
+    let _ = workqueue::system().enqueue(work);
+    waiter.done.wait_for_completion();
+    count as kernel::ffi::c_long
+}
+
+#[repr(transparent)]
+struct SyncFops(kernel::bindings::file_operations);
+unsafe impl Sync for SyncFops {}
+
+static FOPS: SyncFops = SyncFops(kernel::bindings::file_operations {
+    write: Some(unsafe { core::mem::transmute(write_handler as *const ()) }),
+    ..unsafe { core::mem::zeroed() }
+});
+
+struct RustKworkerRemote {
+    dir: *mut kernel::bindings::dentry,
+}
+
+impl kernel::Module for RustKworkerRemote {
+    fn init(_module: &'static ThisModule) -> Result<Self> {
+        let dir = unsafe {
+            kernel::bindings::debugfs_create_dir(
+                c_str!("kcov_dataflow_test").as_char_ptr(),
+                core::ptr::null_mut(),
+            )
+        };
+        unsafe {
+            kernel::bindings::debugfs_create_file_unsafe(
+                c_str!("trigger_kworker_remote").as_char_ptr(),
+                0o222,
+                dir,
+                core::ptr::null_mut(),
+                &FOPS.0,
+            )
+        };
+        Ok(Self { dir })
+    }
+}
+
+impl Drop for RustKworkerRemote {
+    fn drop(&mut self) {
+        unsafe { kernel::bindings::debugfs_remove(self.dir) };
+    }
+}
+
+unsafe impl Send for RustKworkerRemote {}
+unsafe impl Sync for RustKworkerRemote {}
diff --git a/tools/testing/selftests/kcov_dataflow/settings b/tools/testing/selftests/kcov_dataflow/settings
new file mode 100644
index 0000000000000..694d70710ff08
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/kcov_dataflow/test_modules.py b/tools/testing/selftests/kcov_dataflow/test_modules.py
new file mode 100755
index 0000000000000..13cb706a06ff6
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/test_modules.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+test_modules.py - run the kcov_dataflow test modules, one KTAP test each.
+
+Every module is loaded, triggered with recording active and unloaded by
+trigger-view.py's run_capture(). The records that belong to the module are
+then compared with the values its trigger function passes and returns, so a
+test passes only when the instrumented arguments, struct field expansions
+and return values came back intact through the kcov_dataflow buffer. The
+module's call tree is echoed as KTAP diagnostics.
+
+    ./test_modules.py                 # all modules
+    ./test_modules.py -t rust_ffi_contract -C 8 --vmlinux vmlinux
+
+Modules that were not built (no CONFIG_RUST, no toolchain) are reported as
+SKIP; a kernel without /sys/kernel/debug/kcov_dataflow skips everything.
+"""
+import argparse
+import contextlib
+import importlib.util
+import io
+import os
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(HERE, "..", "kselftest"))
+import ksft  # noqa: E402
+
+
+def _load_trigger_view():
+    spec = importlib.util.spec_from_file_location(
+        "trigger_view", os.path.join(HERE, "trigger-view.py"))
+    mod = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(mod)
+    return mod
+
+
+tv = _load_trigger_view()
+
+
+class Check:
+    """Collects expectation failures for one module."""
+
+    def __init__(self):
+        self.failures = []
+
+    def eq(self, what, got, want):
+        if got != want:
+            self.failures.append(f"{what}: got {fmt(got)}, want {fmt(want)}")
+
+    def true(self, what, cond):
+        if not cond:
+            self.failures.append(what)
+
+
+def fmt(v):
+    if isinstance(v, list):
+        return "[" + ", ".join(fmt(x) for x in v) + "]"
+    if isinstance(v, int):
+        return f"0x{v:x}"
+    return str(v)
+
+
+def entries(cap, recs, func):
+    return [r for r in recs if r["type"] == tv.DF_TYPE_ENTRY and func in cap.funcs(r)]
+
+
+def rets(cap, recs, func):
+    return [r["val"] for r in recs if r["type"] == tv.DF_TYPE_RET and func in cap.funcs(r)]
+
+
+def flat_sum(n):
+    """sf_n() returns a->a + b->b + ... over s1..sn: 0x11 + 0x22 + ..."""
+    return sum(0x11 * k for k in range(1, n + 1))
+
+
+def nested_sum(n, _memo={}):
+    """
+    stf_n()/stpf_n() return field0 (0x11 * n) plus the recursive sums of the
+    embedded st1..st(n-1); the same values are used for the value-nested and
+    the pointer-linked towers.
+    """
+    if n not in _memo:
+        _memo[n] = 0x11 * n + sum(nested_sum(k) for k in range(1, n))
+    return _memo[n]
+
+
+def check_struct_family(cap, recs, c, p, flat_ns, stpf8_runs):
+    """
+    Shared expectations for eight_struct_args_c (p="") and
+    eight_struct_args_rust (p="r"): @flat_ns are the sf_N called by the
+    trigger, @stpf8_runs how often the pointer-linked tower is walked.
+    """
+    for n in flat_ns:
+        ents = entries(cap, recs, f"{p}sf_{n}")
+        c.true(f"{p}sf_{n}: ENTRY records", bool(ents))
+        for k in range(n):
+            # arg k is a struct s(k+1) * whose fields are 0x11, 0x22, ...
+            got = [r["vals"] for r in ents if r["arg_idx"] == k]
+            c.true(f"{p}sf_{n} arg[{k}]: ENTRY record", bool(got))
+            for vals in got:
+                c.eq(f"{p}sf_{n} arg[{k}] expanded fields", vals,
+                     [0x11 * (j + 1) for j in range(k + 1)])
+        # rustc may alias identical bodies (rsf_1 == rstf_1), so the RET
+        # list can carry the alias's calls too: check every value.
+        got = rets(cap, recs, f"{p}sf_{n}")
+        c.true(f"{p}sf_{n} RET values all {fmt(flat_sum(n))}: {fmt(got)}",
+               bool(got) and all(v == flat_sum(n) for v in got))
+
+    for fam, calls in ((f"{p}stf", 1), (f"{p}stpf", stpf8_runs)):
+        for n in range(1, 9):
+            got = rets(cap, recs, f"{fam}_{n}")
+            c.true(f"{fam}_{n}: RET records", bool(got))
+            c.true(f"{fam}_{n} RET values all {fmt(nested_sum(n))}: {fmt(got)}",
+                   all(v == nested_sum(n) for v in got))
+        c.eq(f"{fam}_8 RET count", len(rets(cap, recs, f"{fam}_8")), calls)
+
+    for f in (f"{p}sf_fwd", f"{p}sf_fwd_inner"):
+        c.eq(f"{f} RET", rets(cap, recs, f), [flat_sum(4)])
+
+    c.true(f"{p}sf_ret_struct: ENTRY records",
+           bool(entries(cap, recs, f"{p}sf_ret_struct")))
+    c.true(f"{p}sf_ret_struct: RET record",
+           bool(rets(cap, recs, f"{p}sf_ret_struct")))
+
+
+def check_eight_struct_args_c(cap, recs, c):
+    check_struct_family(cap, recs, c, "", range(1, 9), stpf8_runs=2)
+
+
+def check_eight_struct_args_rust(cap, recs, c):
+    check_struct_family(cap, recs, c, "r", (1, 2, 4, 8), stpf8_runs=1)
+
+
+def check_rust_ffi_contract(cap, recs, c):
+    """
+    ffi_alloc_buf(&alloc = {NULL, 0, 0, 0}, 256, 16, is_async=1) records
+    data_size + offsets_size and returns 0 without filling alloc->buffer;
+    ffi_check_result() then sees {NULL, 0x110, 0, 0}. The records must show
+    the violated contract at both boundaries.
+    """
+    ents = entries(cap, recs, "ffi_alloc_buf")
+    by_arg = {r["arg_idx"]: r for r in ents}
+    c.eq("ffi_alloc_buf ENTRY arg indexes", sorted(by_arg), [0, 1, 2, 3])
+    if 0 in by_arg:
+        c.eq("ffi_alloc_buf arg[0] struct ffi_alloc fields",
+             by_arg[0]["vals"], [0, 0, 0, 0])
+    if 1 in by_arg:
+        c.eq("ffi_alloc_buf arg[1] data_size", by_arg[1]["val"], 256)
+    if 2 in by_arg:
+        c.eq("ffi_alloc_buf arg[2] offsets_size", by_arg[2]["val"], 16)
+    if 3 in by_arg:
+        c.eq("ffi_alloc_buf arg[3] is_async", by_arg[3]["val"], 1)
+    c.eq("ffi_alloc_buf RET (claims success)", rets(cap, recs, "ffi_alloc_buf"), [0])
+
+    ents = entries(cap, recs, "ffi_check_result")
+    c.true("ffi_check_result: ENTRY record", bool(ents))
+    for r in ents:
+        c.eq("ffi_check_result arg[0] {buffer NULL: contract violated, "
+             "data_size, free_async, flags}", r["vals"], [0, 0x110, 0, 0])
+    got = rets(cap, recs, "ffi_check_result")
+    c.true(f"ffi_check_result RET -EFAULT: {fmt(got)}",
+           len(got) == 1 and got[0] & 0xffffffff == 0xfffffff2)
+
+
+def check_rust_kworker_remote(cap, recs, c):
+    """
+    The trigger only queues a work item and waits; the records come from the
+    kworker that called kcov_df_remote_start(REMOTE_HANDLE). All three phases
+    of CompositeStore must show up (v0-mangled names keep the method names).
+    """
+    c.true("records captured from the kworker", bool(recs))
+    names = set().union(*(cap.funcs(r) for r in recs)) if recs else set()
+    for phase in ("populate", "update", "drain"):
+        c.true(f"CompositeStore::{phase} recorded",
+               any("CompositeStore" in n and phase in n for n in names))
+
+
+TESTS = (
+    ("rust_ffi_contract", False, check_rust_ffi_contract),
+    ("eight_struct_args_c", False, check_eight_struct_args_c),
+    ("eight_struct_args_rust", False, check_eight_struct_args_rust),
+    ("rust_kworker_remote", True, check_rust_kworker_remote),
+)
+
+
+def diag_tree(cap, recs, vmlinux):
+    out = io.StringIO()
+    with contextlib.redirect_stdout(out):
+        tv.print_tree(recs, cap.syms, vmlinux, {}, cap.ko_path,
+                      cap.mod_text_start)
+    for line in out.getvalue().splitlines():
+        ksft.print_msg(line)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
+    parser.add_argument("-t", "--test", action="append",
+                        help="run only this module (repeatable)")
+    parser.add_argument("-C", "--context", type=int, default=0,
+                        help="echo N records before/after each module record")
+    parser.add_argument("--vmlinux", help="vmlinux for addr2line and KASLR")
+    args = parser.parse_args()
+
+    tests = [t for t in TESTS if not args.test or t[0] in args.test]
+    ksft.print_header()
+    ksft.set_plan(len(tests))
+
+    skip_all = None
+    if not os.path.exists(tv.KCOV_DF_PATH):
+        skip_all = f"{tv.KCOV_DF_PATH} not available (CONFIG_KCOV_DATAFLOW_ARGS/RET)"
+    elif os.geteuid() != 0:
+        skip_all = "must run as root"
+
+    vmlinux = tv.find_vmlinux(args.vmlinux)
+    for name, remote, check in tests:
+        if skip_all:
+            ksft.test_result_skip(f"{name}: {skip_all}")
+            continue
+        ko = tv.find_module(name)
+        if not ko:
+            ksft.test_result_skip(f"{name}: {name}.ko not built")
+            continue
+        try:
+            cap = tv.run_capture(ko, remote=remote, vmlinux=vmlinux,
+                                 log=ksft.print_msg)
+        except OSError as e:
+            ksft.test_result_fail(f"{name}: {e}")
+            continue
+
+        recs = cap.module_records()
+        ksft.print_msg(f"{name}: {cap.total_words} words, {len(cap.records)} "
+                       f"records, {len(recs)} from {name} "
+                       f"(kaslr_offset=0x{cap.kaslr_offset:x})")
+        diag_tree(cap, cap.context_records(args.context) if args.context
+                  else recs, vmlinux)
+
+        c = Check()
+        check(cap, recs, c)
+        for f in c.failures:
+            ksft.print_msg(f"FAIL {name}: {f}")
+        ksft.test_result(not c.failures, name)
+
+    ksft.finished()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/testing/selftests/kcov_dataflow/trigger-view.py b/tools/testing/selftests/kcov_dataflow/trigger-view.py
new file mode 100755
index 0000000000000..b17e49da402d7
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/trigger-view.py
@@ -0,0 +1,755 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+trigger-view.py - Load a test module, trigger it with kcov_dataflow
+recording active, then pretty-print the captured records.
+
+Usage:
+    python3 trigger-view.py eight_struct_args_c
+    python3 trigger-view.py rust_ffi_contract --raw -C 8
+    python3 trigger-view.py rust_kworker_remote --remote
+    python3 trigger-view.py <module> --vmlinux vmlinux --kaslr-offset 0x...
+
+run_capture() does the work and is also what test_modules.py drives:
+  1. Opens /sys/kernel/debug/kcov_dataflow, inits and mmaps the buffer
+  2. Loads the module via finit_module() (its init noise is not recorded)
+  3. Enables recording: KCOV_DF_ENABLE for this task, or with --remote
+     KCOV_DF_REMOTE_ENABLE with handle REMOTE_HANDLE, which the module's
+     kworker opens with kcov_df_remote_start(REMOTE_HANDLE)
+  4. Writes the trigger file(s) the module created under TRIGGER_DIR
+  5. Disables recording and unloads the module
+  6. Parses the records (layout: include/uapi/linux/kcov_dataflow.h)
+
+The CLI then prints them as a call tree, or flat with --raw, with kallsyms
+symbol resolution and addr2line source lines (vmlinux / module .ko).
+
+Recorded PCs have the KASLR offset removed (same as mainline kcov), so
+the runtime offset is derived from /proc/kallsyms and System.map / vmlinux
+(or a per-architecture default) and added back for symbolization; use
+--kaslr-offset to override. Records must contain at least one value word
+and one of the three record types, otherwise the parser resyncs word by
+word (e.g. after a userspace reset of area[0] mid-run).
+"""
+import os
+import sys
+import struct
+import ctypes
+import ctypes.util
+import argparse
+import fcntl
+import platform
+import subprocess
+import shutil
+
+# Constants -- must match include/uapi/linux/kcov_dataflow.h
+DF_TYPE_CMP = 0xC
+DF_TYPE_ENTRY = 0xE
+DF_TYPE_RET = 0xF
+MAGIC_BAD = 0xBADADD85
+BUF_SIZE = 1048576  # 1M words = 8MB
+
+# Record header word: bits 0-23 seq | 28-31 type | 32-47 nvals |
+# 48-55 arg/ret size | 56-63 arg index. Word 1 is the pc (KASLR offset
+# removed, like mainline kcov), word 2 the traced pointer (ENTRY/RET) or the
+# comparison type (CMP), then nvals value words.
+def hdr_seq(h):
+    return h & 0x00FFFFFF
+
+def hdr_type(h):
+    return (h >> 28) & 0xF
+
+def hdr_nvals(h):
+    return (h >> 32) & 0xFFFF
+
+def hdr_size(h):
+    return (h >> 48) & 0xFF
+
+def hdr_arg_idx(h):
+    return (h >> 56) & 0xFF
+
+RECORD_HDR_WORDS = 3
+
+# Runtime KASLR offset (see kaslr_offset()); added back to every recorded pc
+# so /proc/kallsyms lookups work, subtracted again for addr2line on vmlinux.
+KASLR_OFFSET = 0
+
+# Ioctl numbers
+def _IOR(t, nr, size):
+    return (2 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IOW(t, nr, size):
+    return (1 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IO(t, nr):
+    return (ord(t) << 8) | nr
+
+KCOV_DF_INIT_TRACK = _IOR('d', 1, 8)
+KCOV_DF_ENABLE = _IO('d', 100)
+KCOV_DF_DISABLE = _IO('d', 101)
+KCOV_DF_REMOTE_ENABLE = _IOW('d', 102, 8)  # arg: pointer to a __u64 handle
+KCOV_DF_REMOTE_DISABLE = _IO('d', 103)
+
+KCOV_DF_PATH = "/sys/kernel/debug/kcov_dataflow"
+
+# Every test module creates its trigger file(s) in this debugfs directory;
+# writing to them runs the instrumented test functions.
+TRIGGER_DIR = "/sys/kernel/debug/kcov_dataflow_test"
+
+# Remote handle registered with KCOV_DF_REMOTE_ENABLE; must match the
+# kcov_df_remote_start(1) call in the rust_kworker_remote test module
+# (KCOV_SUBSYSTEM_COMMON, instance 1).
+REMOTE_HANDLE = 1
+
+# syscall numbers
+_machine = platform.machine()
+if _machine == "aarch64":
+    SYS_FINIT_MODULE = 273
+    SYS_DELETE_MODULE = 106
+else:  # x86_64
+    SYS_FINIT_MODULE = 313
+    SYS_DELETE_MODULE = 176
+
+SELFTEST_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def load_kallsyms():
+    """Load kernel symbols for PC resolution."""
+    syms = []
+    try:
+        with open("/proc/kallsyms") as f:
+            for line in f:
+                parts = line.split()
+                if len(parts) >= 3:
+                    addr = int(parts[0], 16)
+                    name = parts[2]
+                    mod = parts[3].strip("[]") if len(parts) > 3 else ""
+                    syms.append((addr, name, mod))
+    except (PermissionError, FileNotFoundError):
+        pass
+    syms.sort()
+    return syms
+
+
+def runtime_text(syms):
+    """Runtime address of _text from kallsyms, 0 if hidden."""
+    return next((a for a, n, m in syms if n == "_text" and not m), 0)
+
+
+# Link-time address of _text per architecture, used only when neither
+# System.map nor vmlinux is available: x86_64 __START_KERNEL
+# (__START_KERNEL_map + CONFIG_PHYSICAL_START), arm64 KIMAGE_VADDR.
+LINKTIME_TEXT_DEFAULT = {
+    "x86_64": 0xffffffff81000000,
+    "aarch64": 0xffff800080000000,
+}
+
+
+def linktime_text(vmlinux=None):
+    """Return (link-time address of _text, source description) or (0, "")."""
+    rel = os.uname().release
+    candidates = []
+    if vmlinux:
+        candidates.append(os.path.join(os.path.dirname(vmlinux) or ".", "System.map"))
+    candidates += ["System.map", f"/boot/System.map-{rel}",
+                   f"/usr/lib/debug/boot/System.map-{rel}"]
+    for sm in candidates:
+        try:
+            with open(sm) as f:
+                for line in f:
+                    parts = line.split()
+                    if len(parts) == 3 and parts[2] == "_text":
+                        return int(parts[0], 16), sm
+        except (OSError, ValueError):
+            continue
+    if vmlinux and shutil.which("nm"):
+        try:
+            r = subprocess.run(["nm", "--defined-only", vmlinux],
+                               capture_output=True, text=True, timeout=300)
+            for line in r.stdout.splitlines():
+                parts = line.split()
+                if len(parts) == 3 and parts[2] == "_text":
+                    return int(parts[0], 16), f"nm {vmlinux}"
+        except (OSError, subprocess.TimeoutExpired):
+            pass
+    link = LINKTIME_TEXT_DEFAULT.get(platform.machine(), 0)
+    return link, f"{platform.machine()} default" if link else ""
+
+
+def kaslr_offset(syms, vmlinux=None):
+    """
+    Runtime KASLR offset: recorded PCs have it removed (kcov's
+    canonicalize_ip()), /proc/kallsyms has it applied. Computed as the
+    runtime _text (kallsyms) minus the link-time _text (System.map, nm
+    vmlinux, or the architecture default). KASLR offsets are 2 MiB aligned
+    on x86_64 and arm64, which is used as a sanity check on the result.
+    """
+    runtime = runtime_text(syms)
+    if not runtime:
+        print("# warning: _text not in /proc/kallsyms (kptr_restrict?); "
+              "PCs will not symbolize", file=sys.stderr)
+        return 0
+    link, source = linktime_text(vmlinux)
+    if not link:
+        print(f"# warning: no System.map/vmlinux and no default _text for "
+              f"{platform.machine()}; pass --kaslr-offset", file=sys.stderr)
+        return 0
+    off = runtime - link
+    if off % (2 << 20):
+        print(f"# warning: kaslr offset 0x{off:x} from {source} is not 2 MiB "
+              f"aligned; check CONFIG_PHYSICAL_START/KIMAGE_VADDR or pass "
+              f"--kaslr-offset", file=sys.stderr)
+    return off
+
+
+# Rust symbol demangling via llvm-cxxfilt or rustfilt
+_demangler = None
+
+def _init_demangler():
+    global _demangler
+    for tool in ["llvm-cxxfilt", "rustfilt", "c++filt"]:
+        path = shutil.which(tool)
+        if path:
+            _demangler = path
+            return
+    _demangler = ""
+
+_demangled = {}
+
+def demangle(name):
+    """Demangle a Rust/C++ symbol name (memoized: one process per name)."""
+    global _demangler
+    if _demangler is None:
+        _init_demangler()
+    if not _demangler or not name.startswith("_R"):
+        return name
+    if name not in _demangled:
+        try:
+            r = subprocess.run([_demangler, name], capture_output=True,
+                               text=True, timeout=2)
+            _demangled[name] = r.stdout.strip() if r.returncode == 0 else name
+        except (OSError, subprocess.TimeoutExpired):
+            _demangled[name] = name
+    return _demangled[name]
+
+
+def find_vmlinux(vmlinux=None):
+    """Locate vmlinux for addr2line: explicit path, else the usual places."""
+    if vmlinux:
+        return vmlinux
+    for p in ["vmlinux", "/boot/vmlinux", "/usr/lib/debug/boot/vmlinux"]:
+        if os.path.exists(p):
+            return p
+    return None
+
+
+def _a2l_target(pc, vmlinux, ko_path, mod_text_base):
+    """(binary, address in it) to symbolize pc with, or None."""
+    if ko_path and mod_text_base and pc >= mod_text_base:
+        return ko_path, pc - mod_text_base
+    if vmlinux:
+        return vmlinux, pc - KASLR_OFFSET  # vmlinux holds link-time addresses
+    return None
+
+
+def resolve_lines(pcs, vmlinux, cache, ko_path=None, mod_text_base=0):
+    """
+    Resolve every pc in @pcs to file:line into @cache, one addr2line run
+    per binary: a DWARF5 vmlinux takes hundreds of ms to open, so one
+    process per record does not scale to thousands of records.
+    """
+    todo = {}
+    for pc in pcs:
+        if pc in cache:
+            continue
+        cache[pc] = ""
+        tgt = _a2l_target(pc, vmlinux, ko_path, mod_text_base)
+        if tgt:
+            todo.setdefault(tgt[0], []).append((pc, tgt[1]))
+    for binary, pairs in todo.items():
+        try:
+            r = subprocess.run(
+                ["addr2line", "-e", binary] + [f"0x{a:x}" for _, a in pairs],
+                capture_output=True, text=True, timeout=300)
+        except (subprocess.TimeoutExpired, FileNotFoundError):
+            continue
+        for (pc, _), loc in zip(pairs, r.stdout.splitlines()):
+            loc = loc.strip()
+            if loc and loc != "??:0" and loc != "??:?":
+                # Shorten path: keep only filename:line
+                cache[pc] = loc.rsplit("/", 1)[-1]
+
+
+def resolve_line(pc, vmlinux, cache, ko_path=None, mod_text_base=0):
+    """Resolve one PC to source file:line using addr2line (cached)."""
+    if pc not in cache:
+        resolve_lines([pc], vmlinux, cache, ko_path, mod_text_base)
+    return cache[pc]
+
+
+def get_kernel_meta():
+    """Collect kernel build metadata."""
+    meta = {"release": os.uname().release}
+    try:
+        with open("/proc/version") as f:
+            v = f.read().strip()
+        meta["version"] = v
+        # Extract compiler version
+        if "gcc" in v.lower():
+            meta["compiler"] = v.split("(")[1].split(")")[0] if "(" in v else ""
+        elif "clang" in v.lower():
+            idx = v.lower().find("clang")
+            meta["compiler"] = v[idx:idx+30].split(")")[0]
+    except OSError:
+        pass
+    return meta
+
+
+def print_kernel_meta(meta, ko_path=None):
+    """Print kernel metadata header/footer."""
+    print(f"# {'=' * 60}")
+    print(f"# Kernel: {meta.get('release', 'unknown')}")
+    print(f"# Build:  {meta.get('version', 'unknown')[:80]}")
+    if meta.get('compiler'):
+        print(f"# Compiler: {meta['compiler']}")
+    # Read rustc version from .ko .comment section
+    if ko_path:
+        try:
+            r = subprocess.run(
+                ["readelf", "-p", ".comment", ko_path],
+                capture_output=True, text=True, timeout=5)
+            for line in r.stdout.splitlines():
+                if "rustc" in line:
+                    ver = line.split("]", 1)[-1].strip()
+                    print(f"# Rustc: {ver}")
+                    break
+        except (OSError, subprocess.TimeoutExpired):
+            pass
+    print(f"# {'=' * 60}")
+
+
+def lookup(pc, syms):
+    """Nearest kallsyms entry <= pc as (name, offset, module) or None."""
+    if not syms:
+        return None
+    lo, hi = 0, len(syms) - 1
+    while lo < hi:
+        mid = (lo + hi + 1) // 2
+        if syms[mid][0] <= pc:
+            lo = mid
+        else:
+            hi = mid - 1
+    addr, name, mod = syms[lo]
+    if addr > pc:
+        return None
+    return name, pc - addr, mod
+
+
+def symbolize(pc, syms):
+    """Find nearest symbol <= pc. Returns (display_name, module_tag)."""
+    hit = lookup(pc, syms)
+    if not hit:
+        return f"0x{pc:x}", ""
+    name, offset, mod = hit
+    dname = demangle(name)
+    display = f"{dname}+0x{offset:x}" if offset else dname
+    return display, f" [{mod}]" if mod else ""
+
+
+def format_val(v):
+    """Format a captured value."""
+    if v == MAGIC_BAD:
+        return "FAULT"
+    if v == 0:
+        return "0x0"
+    return f"0x{v:x}"
+
+
+def find_module(name):
+    """
+    Find the .ko for test @name: <name>/<name>.ko in the source tree, or
+    <name>.ko next to this script in an installed (make install) tree.
+    """
+    for ko_path in (os.path.join(SELFTEST_DIR, name, f"{name}.ko"),
+                    os.path.join(SELFTEST_DIR, f"{name}.ko")):
+        if os.path.exists(ko_path):
+            return ko_path
+    return None
+
+
+def finit_module(ko_path):
+    """Load a kernel module via finit_module syscall."""
+    libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+    fd = os.open(ko_path, os.O_RDONLY)
+    ret = libc.syscall(SYS_FINIT_MODULE, fd, b"", 0)
+    os.close(fd)
+    if ret != 0:
+        errno = ctypes.get_errno()
+        raise OSError(errno, f"finit_module({ko_path}): {os.strerror(errno)}")
+
+
+def delete_module(name):
+    """Unload a kernel module."""
+    libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+    ret = libc.syscall(SYS_DELETE_MODULE, name.encode(), 0)
+    if ret != 0:
+        errno = ctypes.get_errno()
+        raise OSError(errno, f"delete_module({name}): {os.strerror(errno)}")
+
+
+def trigger_module():
+    """
+    Write to every trigger file the loaded module created under TRIGGER_DIR.
+    Opened without O_CREAT: debugfs directories have no ->create, so a
+    "w"-mode open of a missing name fails with EOPNOTSUPP, not ENOENT.
+    """
+    try:
+        names = sorted(os.listdir(TRIGGER_DIR))
+    except OSError:
+        names = []
+    hits = []
+    for n in names:
+        path = os.path.join(TRIGGER_DIR, n)
+        try:
+            fd = os.open(path, os.O_WRONLY)
+        except OSError:
+            continue
+        try:
+            os.write(fd, b"1")
+        finally:
+            os.close(fd)
+        hits.append(path)
+    if not hits:
+        raise FileNotFoundError(f"no trigger file under {TRIGGER_DIR}")
+    return hits
+
+
+def parse_records(buf, total_words):
+    """Parse the ring buffer into a list of records."""
+    records = []
+    pos = 1
+    end = min(1 + total_words, BUF_SIZE)
+    while pos + RECORD_HDR_WORDS <= end:
+        hdr = buf[pos]
+        rtype = hdr_type(hdr)
+        num_vals = hdr_nvals(hdr)
+
+        # Every record the kernel writes has nvals >= 1 and a known type;
+        # anything else is garbage (e.g. a userspace reset mid-run): resync.
+        if rtype not in (DF_TYPE_ENTRY, DF_TYPE_RET, DF_TYPE_CMP) \
+                or num_vals == 0 or pos + RECORD_HDR_WORDS + num_vals > end:
+            pos += 1
+            continue
+
+        pc = int(buf[pos + 1]) + KASLR_OFFSET
+        ptr = int(buf[pos + 2])  # ENTRY/RET: traced pointer; CMP: cmp type
+        if rtype == DF_TYPE_CMP:
+            pos += RECORD_HDR_WORDS + num_vals
+            continue
+
+        # Valid records always have a non-zero PC (kernel text address)
+        if pc == 0:
+            pos += 1
+            continue
+
+        vals = [int(buf[pos + RECORD_HDR_WORDS + vi]) for vi in range(num_vals)]
+        records.append({
+            "type": rtype,
+            "seq": hdr_seq(hdr),
+            "pc": pc,
+            "ptr": ptr,
+            "arg_idx": hdr_arg_idx(hdr),
+            "size": hdr_size(hdr),
+            "val": vals[0],
+            "vals": vals,
+        })
+        pos += RECORD_HDR_WORDS + num_vals
+    return records
+
+
+class Capture:
+    """Everything run_capture() collected for one module run."""
+
+    def __init__(self, ko_path, mod_name, records, syms, total_words,
+                 mod_text_start, kaslr_off):
+        self.ko_path = ko_path
+        self.mod_name = mod_name
+        self.records = records
+        self.syms = syms
+        self.total_words = total_words
+        self.mod_text_start = mod_text_start
+        self.kaslr_offset = kaslr_off
+        self.runtime_text = runtime_text(syms)
+        self._mod_syms = any(m == mod_name for _, _, m in syms)
+        # Aliases: rustc's merge-functions makes identical bodies (e.g. the
+        # one-field rsf_1 and rstf_1) share one address, so a PC can carry
+        # several names.
+        self._names = {}
+        for addr, name, mod in syms:
+            self._names.setdefault((addr, mod), set()).add(name)
+
+    def is_module_pc(self, pc):
+        """True if pc lies in the test module (kallsyms, else .text start)."""
+        if self._mod_syms:
+            hit = lookup(pc, self.syms)
+            return bool(hit) and hit[2] == self.mod_name
+        # Fallback: if no module symbols (kptr_restrict), use .text start
+        return bool(self.mod_text_start) and pc >= self.mod_text_start
+
+    def funcs(self, rec):
+        """All raw kallsyms names of the function a record belongs to."""
+        hit = lookup(rec["pc"], self.syms)
+        if not hit:
+            return set()
+        name, offset, mod = hit
+        return self._names.get((rec["pc"] - offset, mod), {name})
+
+    def module_records(self):
+        return [r for r in self.records if self.is_module_pc(r["pc"])]
+
+    def context_records(self, n):
+        """Module records plus n records before/after each of them."""
+        keep = set()
+        for i, r in enumerate(self.records):
+            if self.is_module_pc(r["pc"]):
+                keep.update(range(max(0, i - n),
+                                  min(len(self.records), i + n + 1)))
+        return [self.records[i] for i in sorted(keep)]
+
+
+def run_capture(ko_path, remote=False, vmlinux=None, kaslr_override=None,
+                log=None):
+    """
+    Load @ko_path, record while its trigger file(s) are written, unload it
+    and return a Capture. @remote publishes the buffer for REMOTE_HANDLE
+    instead of enabling recording for this task. Raises OSError.
+    """
+    global KASLR_OFFSET
+    log = log or (lambda msg: print(f"# {msg}"))
+
+    # Ensure kallsyms shows real addresses
+    try:
+        with open("/proc/sys/kernel/kptr_restrict", "w") as f:
+            f.write("0")
+    except OSError:
+        pass
+
+    df_fd = os.open(KCOV_DF_PATH, os.O_RDWR)
+    try:
+        # Init + mmap
+        fcntl.ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)
+        libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+        libc.mmap.restype = ctypes.c_void_p
+        libc.mmap.argtypes = [
+            ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
+            ctypes.c_int, ctypes.c_int, ctypes.c_long
+        ]
+        buf_ptr = libc.mmap(None, BUF_SIZE * 8, 0x3, 0x01, df_fd, 0)
+        if buf_ptr == ctypes.c_void_p(-1).value:
+            errno = ctypes.get_errno()
+            raise OSError(errno, f"mmap: {os.strerror(errno)}")
+        buf = (ctypes.c_uint64 * BUF_SIZE).from_address(buf_ptr)
+
+        # Load module first (its init generates noise with INSTRUMENT_ALL)
+        mod_name = os.path.basename(ko_path).replace(".ko", "")
+        finit_module(ko_path)
+        log(f"Loaded {mod_name}")
+        try:
+            # Module .text address, the PC filter fallback without kallsyms
+            mod_text_start = 0
+            try:
+                with open(f"/sys/module/{mod_name}/sections/.text") as f:
+                    mod_text_start = int(f.read().strip(), 16)
+            except (OSError, ValueError):
+                pass
+
+            # Enable recording AFTER load, BEFORE trigger (no loader noise).
+            # Remote: the handle is passed by pointer (a __u64 in a buffer),
+            # so the full 64-bit value survives 32-bit/compat callers.
+            if remote:
+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_ENABLE,
+                            struct.pack("Q", REMOTE_HANDLE))
+            else:
+                fcntl.ioctl(df_fd, KCOV_DF_ENABLE, 0)
+            buf[0] = 0
+            try:
+                for path in trigger_module():
+                    log(f"Triggered {path}")
+            finally:
+                fcntl.ioctl(df_fd, KCOV_DF_REMOTE_DISABLE if remote
+                            else KCOV_DF_DISABLE, 0)
+
+            # Read kallsyms while the module is still loaded
+            syms = load_kallsyms()
+        finally:
+            try:
+                delete_module(mod_name)
+            except OSError as e:
+                log(f"warning: {e}")
+
+        if kaslr_override is not None:
+            KASLR_OFFSET = kaslr_override
+        else:
+            KASLR_OFFSET = kaslr_offset(syms, find_vmlinux(vmlinux))
+
+        total = int(buf[0])
+        records = parse_records(buf, total)
+        return Capture(ko_path, mod_name, records, syms, total,
+                       mod_text_start, KASLR_OFFSET)
+    finally:
+        os.close(df_fd)
+
+
+def print_raw(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+    """Print records in raw format with source line on left."""
+    if cache is None:
+        cache = {}
+    # Pre-resolve all locations (one addr2line run) to find max width
+    resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+                  mod_text_base)
+    locs = [cache[r["pc"]] for r in records]
+    max_w = max((len(l) for l in locs if l), default=0)
+    max_w = max(max_w, 10)  # minimum width
+
+    for i, r in enumerate(records):
+        name, mod = symbolize(r["pc"], syms)
+        sym = f"{name}{mod}"
+        t = "ENTRY" if r["type"] == DF_TYPE_ENTRY else "RET  "
+        arg_idx = r["arg_idx"]
+        size = r["size"]
+        left = f"{locs[i]:>{max_w}s}" if locs[i] else f"{'':>{max_w}s}"
+        vals = format_val(r["val"]) if len(r["vals"]) == 1 else \
+            "{" + ", ".join(format_val(v) for v in r["vals"]) + "}"
+        print(f"{left}   [{t}] seq={r['seq']:3d} {sym} "
+              f"arg[{arg_idx}]({size}) @0x{r['ptr']:x} = {vals}")
+
+
+def print_tree(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+    """Print records as indented call tree with source line on left."""
+    if cache is None:
+        cache = {}
+    # Pre-resolve all PCs (one addr2line run) for alignment
+    resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+                  mod_text_base)
+    max_w = max((len(v) for v in cache.values() if v), default=10)
+    max_w = max(max_w, 10)
+
+    depth = 0
+    call_stack = []  # Stack of (name, mod, args_str, pc) for matching returns
+    i = 0
+    while i < len(records):
+        r = records[i]
+        name, mod = symbolize(r["pc"], syms)
+
+        if r["type"] == DF_TYPE_ENTRY:
+            # Collect all args for this call (same PC, consecutive entries);
+            # order by index, as the pass emits dead-arg traces last.
+            args = []
+            pc = r["pc"]
+            while i < len(records) and records[i]["type"] == DF_TYPE_ENTRY \
+                    and records[i]["pc"] == pc:
+                vals = records[i]["vals"]
+                if len(vals) > 1:
+                    fields = ", ".join(format_val(v) for v in vals)
+                    args.append((records[i]["arg_idx"], "{" + fields + "}"))
+                else:
+                    args.append((records[i]["arg_idx"],
+                                 format_val(records[i]["val"])))
+                i += 1
+            args_str = ", ".join(a for _, a in sorted(args, key=lambda x: x[0]))
+            call_stack.append((name, mod, args_str, pc))
+            depth += 1
+        else:
+            # Pop void calls (no return record) until we find matching PC
+            while call_stack and call_stack[-1][3] != r["pc"]:
+                depth = max(0, depth - 1)
+                indent = "  " * depth
+                vname, vmod, vargs, vpc = call_stack.pop()
+                loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+                left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+                print(f"{left}   {indent}{vname}({vargs}){vmod}")
+            depth = max(0, depth - 1)
+            indent = "  " * depth
+            ret_size = r["size"]
+            loc = resolve_line(r["pc"], vmlinux, cache, ko_path, mod_text_base)
+            left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+            if call_stack:
+                cname, cmod, cargs, _ = call_stack.pop()
+                if ret_size == 0:
+                    print(f"{left}   {indent}{cname}({cargs}){cmod}")
+                else:
+                    print(f"{left}   {indent}{format_val(r['val'])} = {cname}({cargs}){cmod}")
+            else:
+                if ret_size == 0:
+                    print(f"{left}   {indent}{name}(){mod}")
+                else:
+                    print(f"{left}   {indent}{format_val(r['val'])} = {name}(){mod}")
+            i += 1
+
+    # Flush remaining void calls on the stack
+    while call_stack:
+        depth = max(0, depth - 1)
+        indent = "  " * depth
+        vname, vmod, vargs, vpc = call_stack.pop()
+        loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+        left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+        print(f"{left}   {indent}{vname}({vargs}){vmod}")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Load a test module with kcov_dataflow and view records")
+    parser.add_argument("module", help="Test module name (e.g. eight_struct_args_c)")
+    parser.add_argument("--raw", action="store_true",
+                        help="Print raw records instead of tree")
+    parser.add_argument("--ko", help="Explicit path to .ko file")
+    parser.add_argument("--context", "-C", type=int, default=0,
+                        help="Show N records before/after each module record")
+    parser.add_argument("--vmlinux", help="Path to vmlinux for addr2line")
+    parser.add_argument("--remote", action="store_true",
+                        help="Use KCOV_DF_REMOTE_ENABLE for kworker capture")
+    parser.add_argument("--kaslr-offset", type=lambda x: int(x, 0),
+                        help="Override the runtime KASLR offset added to PCs")
+    args = parser.parse_args()
+
+    ko_path = args.ko or find_module(args.module)
+    if not ko_path or not os.path.exists(ko_path):
+        print(f"Cannot find module for '{args.module}'", file=sys.stderr)
+        print("Build it first: make -C tools/testing/selftests "
+              "TARGETS=kcov_dataflow LLVM=1 CC=clang", file=sys.stderr)
+        sys.exit(1)
+
+    try:
+        cap = run_capture(ko_path, remote=args.remote, vmlinux=args.vmlinux,
+                          kaslr_override=args.kaslr_offset)
+    except OSError as e:
+        print(f"{args.module}: {e}", file=sys.stderr)
+        sys.exit(1)
+
+    print(f"# Captured {cap.total_words} words (kaslr_offset=0x{cap.kaslr_offset:x}, "
+          f"_text=0x{cap.runtime_text:x})")
+    print(f"# {len(cap.records)} records")
+
+    if cap.syms or cap.mod_text_start:
+        if args.context > 0:
+            records = cap.context_records(args.context)
+            print(f"# showing {len(records)} records with context={args.context} "
+                  f"around {cap.mod_name}\n")
+        else:
+            records = cap.module_records()
+            print(f"# {len(records)} from {cap.mod_name}\n")
+    else:
+        records = cap.records
+        print("")
+
+    meta = get_kernel_meta()
+    print_kernel_meta(meta, ko_path=ko_path)
+
+    vmlinux = find_vmlinux(args.vmlinux)
+    show = print_raw if args.raw else print_tree
+    show(records, cap.syms, vmlinux, {}, ko_path, cap.mod_text_start)
+
+    print_kernel_meta(meta, ko_path=ko_path)
+
+
+if __name__ == "__main__":
+    main()
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 0000000000000..1cb3d9b41c070
--- /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 0000000000000..55072de189d31
--- /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 0000000000000..d7b04c368ced9
--- /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

13/3 2026/09/03 13:17 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 75145
							output: 342
							thoughts: 1902
						

Thoughts:
**KMSAN Assessment: KCOV-Dataflow Patch**

Okay, let's break this down. My task is to decide if this KCOV-Dataflow patch warrants a KMSAN fuzzing session. I'm an expert, so I know the key distinction: KASAN already covers a lot – OOB, UAF, double-frees, locking issues, and memory corruption. KMSAN is specifically about detecting reads of *uninitialized* memory, both in kernel-to-user leaks and within the kernel itself. The rule is clear: if KASAN (or LOCKDEP or UBSAN) can catch the bug, we *don't* need KMSAN.

My initial impression is that this patch introduces a new tracing functionality. It's essentially a kernel dataflow tracing mechanism that writes function arguments, return values, and comparisons to either a per-task ring buffer or a remote kworker ring buffer, then maps it to userspace via `mmap`. This immediately makes me think about potential uninitialized memory issues: does the patch accidentally leak uninitialized kernel data to userspace, or does it read uninitialized memory?

Let's delve into the details. I see that the `kcov_dataflow.c` module is the core of this. Firstly, in the `Makefile`, I see that all sanitizers are disabled: `KMSAN_SANITIZE_kcov_dataflow.o := n`. This is important, but not a deal-breaker, as it's common practice for core tracing modules. It means the developers have considered these issues.

Now, let's focus on the allocation and copying functions. I see multiple uses of `kzalloc_obj`, which guarantees zero-initialization of the allocated memory. The critical `vmalloc_user` function is used for buffer allocation, which also zeroes the pages upon allocation. Crucially, the code reads data from kernel memory in several places using `copy_from_kernel_nofault` or `get_kernel_nofault`. I noticed that a zero variable is initialized before a memory copy to it, so that we don't accidentally read an uninitialized variable to user space. This is a very good sign. Additionally, any struct offsets have initialized values.

I carefully scan for any `copy_to_user` or `put_user` calls. I spot `get_user` for an input `handle`, and `vm_insert_page` to write to the userspace mmap. The latter does zero pages. Are there any uninitialized kernel data structures being leaked to the user space? No, I don't see any, and that is a major relief.

Going deeper, I look for potential memory leaks, especially when structs are being copied to userspace. In the critical `kcov_df_write`, I see that the kernel code writes various values (header, program counter, pointer values). All the values seem to be explicitly calculated and initialized before being written to the ring buffer. Nothing looks uninitialized, and I am especially pleased that the variable used to store the values for the copies is initialized to zero.

The initialization functions, like `kcov_dataflow_task_init` and `fork.c` functions, always seem to zero all struct fields. Again, a good sign, and a safe practice.

Essentially, I'm confident that all the allocations use `kzalloc` or `vmalloc_user` to provide zeroed memory. The core logic explicitly initializes variables before copying kernel memory. I don't see any conditional logic depending on uninitialized data. Furthermore, the explicit disabling of KMSAN instrumentation for this specific module strongly implies that the developers have carefully considered memory initialization.

I'm confident. This patch series doesn't introduce any new uninitialized memory risks. Any concurrency or boundary problems would likely be caught by KASAN or LOCKDEP anyway.

Therefore, `NeedsKMSAN = false`. I'm calling `set-results`.



14/3 2026/09/03 13:17 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces KCOV-Dataflow, adding new infrastructure to capture function arguments, return values, and comparison operands into mmap-able per-task and remote buffers.\n\nAnalysis for uninitialized memory risks:\n1. Heap objects (`struct kcov_dataflow`, `struct kcov_df_remote`) are allocated using `kzalloc_obj()`, ensuring all fields are zero-initialized.\n2. The user-accessible dataflow buffer is allocated via `vmalloc_user()`, which automatically zeroes memory pages.\n3. In data collection callbacks (`kcov_df_write()`, `kcov_df_trace_cmp()`), all record fields, headers, and values are explicitly initialized (`val = 0`, etc.) before being populated via safe accessors (`copy_from_kernel_nofault()`, `get_kernel_nofault()`).\n4. Per-task dataflow fields in `task_struct` are explicitly initialized in `kcov_dataflow_task_init()`.\n5. No uninitialized memory or struct padding is copied to user space or evaluated in branching decisions.\n6. `kernel/kcov_dataflow.c` explicitly disables KMSAN instrumentation (`KMSAN_SANITIZE_kcov_dataflow.o := n`).\n7. Potential concurrency, bounds, and refcounting issues fall strictly under the domain of KASAN, LOCKDEP, and standard kernel debugging facilities.\n\nTherefore, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces KCOV-Dataflow, adding new infrastructure to capture function arguments, return values, and comparison operands into mmap-able per-task and remote buffers.\n\nAnalysis for uninitialized memory risks:\n1. Heap objects (`struct kcov_dataflow`, `struct kcov_df_remote`) are allocated using `kzalloc_obj()`, ensuring all fields are zero-initialized.\n2. The user-accessible dataflow buffer is allocated via `vmalloc_user()`, which automatically zeroes memory pages.\n3. In data collection callbacks (`kcov_df_write()`, `kcov_df_trace_cmp()`), all record fields, headers, and values are explicitly initialized (`val = 0`, etc.) before being populated via safe accessors (`copy_from_kernel_nofault()`, `get_kernel_nofault()`).\n4. Per-task dataflow fields in `task_struct` are explicitly initialized in `kcov_dataflow_task_init()`.\n5. No uninitialized memory or struct padding is copied to user space or evaluated in branching decisions.\n6. `kernel/kcov_dataflow.c` explicitly disables KMSAN instrumentation (`KMSAN_SANITIZE_kcov_dataflow.o := n`).\n7. Potential concurrency, bounds, and refcounting issues fall strictly under the domain of KASAN, LOCKDEP, and standard kernel debugging facilities.\n\nTherefore, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)