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

