B EYOND E DGE C OVERAGE : P ER -TASK DATA -F LOW E XTRACTION AT K ERNEL F UNCTION B OUNDARIES VIA LLVM
Yunseong Kim Ericsson Software Technology [email protected]
A BSTRACT
PR
arXiv:2606.00455v1 [cs.CR] 30 May 2026
A P REPRINT
Coverage-guided kernel fuzzers such as syzkaller rely on edge coverage (trace-pc) as their sole feedback signal. This context-blind approach cannot distinguish execution paths that differ only in argument values—for example, two invocations of copy_from_user() with different size parameters hit identical basic blocks yet have vastly different security implications. We present KCOV- DATAFLOW, an LLVM-based instrumentation framework that extends Linux KCOV with data-flow extraction of function arguments and return values. A compiler pass (-fsanitize-coverage=dataflow-args,dataflow-ret) emits lightweight callbacks capturing a structured tuple ⟨PC , arg_idx , arg_size, ptr , offsets[]⟩ at function entry and ⟨PC , ret_size, ptr , offsets[]⟩ at return. Composite types are automatically decomposed via DWARF DICompositeType metadata with zero source annotation. A separate kernel device (/sys/kernel/debug/kcov_dataflow) provides lock-free per-task ring buffers with no interference to existing KCOV or syzkaller infrastructure. We demonstrate dual utility: (1) fuzzers gain state-aware feedback for mutation guidance into value-dependent state transitions, and (2) security analysts obtain deterministic argument records for root-cause analysis without printk or kprobe overhead. A post-compilation pipeline (rustc→opt→llc) enables Rust kernel module instrumentation without modifying rustc—the only runtime method for capturing Rust function arguments given that drgn/vmcore fails under -O2 DWARF elision. Evaluated on five vulnerability classes (OOB, UAF, double-free, 10-deep chain propagation, Rust FFI) with <3% overhead on instrumented paths.
Keywords Kernel Fuzzing · Data-Flow Extraction · LLVM Instrumentation · KCOV · Rust for Linux
1
Introduction
The Linux kernel exceeds 30 million lines of code, integrates Rust as a second systems language, and implements complex locking hierarchies (rwsem, mutex, RCU) that create deep state spaces unreachable by coverage-guided fuzzing alone. Syzkaller [1] has discovered over 5,000 kernel bugs, yet plateaus on stateful subsystems such as ksmbd, io_uring, and binder where security-critical behavior depends on runtime argument values rather than control-flow topology. The fundamental limitation is a semantic gap: KCOV’s trace-pc reports which basic blocks executed but not with what values. Two executions of vfs_open() with different access-mode flags traverse identical edges yet have entirely different security implications. Fuzzers cannot distinguish these without data-flow context, leading to coverage saturation on value-dependent state transitions.
Prior approaches each fall short for kernel-scale deployment. Dynamic tracing via ftrace/kprobes imposes perfunction overhead unsuitable for continuous fuzzing. IJON [2] requires manual annotation—infeasible for 30M LOC. Context-sensitive coverage (Angora [3], CollAFL [4]) clones call sites but explodes on polymorphic dispatch (file_operations alone has 20+ function pointers). DataFlowSanitizer provides byte-level taint tracking but imposes 10–100× overhead, rendering it unusable for kernel fuzzing.
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
We observe that function boundaries are natural chokepoints where data-flow context is both cheap to capture and highly informative. Arguments encode the intent of a call; return values encode the outcome. Capturing these at compile time with a lock-free kernel transport gives both fuzzers and analysts a new dimension of observability without source annotation. KCOV- DATAFLOW is an automated LLVM IR instrumentation framework that captures the data-flow tuple ⟨PC caller , arg_idx × arg_size, ptr , offsets[]⟩ at every function boundary. It operates through three layers: (1) a compiler pass that emits callbacks with struct field expansion via DWARF metadata, (2) a kernel-resident lock-free per-task ring buffer delivered through a separate /sys/kernel/debug/kcov_dataflow device, and (3) a user-space consumer interface for both fuzzer executors and human analysts.
Our contributions are:
PR
• An LLVM SanitizerCoverage extension that captures function arguments and return values with automatic struct field expansion via DICompositeType metadata, requiring zero source annotation. • A lock-free, per-task kernel transport mechanism that operates safely in process context with explicit interrupt rejection, without interfering with existing KCOV or syzkaller infrastructure. • A post-compilation pipeline enabling Rust kernel module instrumentation without modifying the Rust compiler toolchain. • Empirical demonstration on five vulnerability classes showing that boundary data-flow context provides information unavailable through any combination of existing tools (coverage + sanitizers + post-mortem debuggers).
2
Background & Motivation
2.1
KCOV and Coverage-Guided Kernel Fuzzing
KCOV [5] instruments the kernel at compile time via SanitizerCoverage’s trace-pc callback. Each task owns a memory-mapped buffer into which the kernel writes the program counter of every executed edge. User-space fuzzers (primarily syzkaller) hash these PCs into a coverage bitmap analogous to AFL’s virgin_bits map, using novel coverage as the signal to retain inputs in the corpus. This architecture has proven remarkably effective: syzkaller continuously discovers 100+ bugs per month across upstream kernels. However, coverage saturates on stateful subsystems. After initial exploration, new edges become rare, and the fuzzer degenerates into random mutation without meaningful guidance. 2.2
The Data-Flow Blind Spot
Consider the ksmbd SMB2_CREATE handler. The same control-flow edges are traversed for different file access modes, but the security-relevant difference lies in the flags argument to vfs_open(). A fuzzer guided solely by edge coverage cannot distinguish a benign read-only open from a dangerous write-with-truncate—both produce identical coverage traces.
A second example arises at Rust-to-C FFI boundaries. A Rust wrapper calls a C function with a struct pointer whose fields determine kernel behavior. Edge coverage sees one call site; the struct contents—which control whether a privilege check is bypassed—are invisible. 2.3
Threat Model and Assumptions
We assume a standard kernel fuzzing threat model: an unprivileged user-space process with access to the syscall interface attempts to trigger memory corruption in kernel subsystems. KASAN/KMSAN detect the corruption once triggered; our tool provides the path to triggering it and the data for triaging it. We assume the kernel is compiled with debug information (-g) for full struct field expansion, though the system degrades gracefully to scalar-only capture without it.
3
System Design & Architecture
3.1
Overview
KCOV- DATAFLOW operates as a three-layer pipeline: (1) compile-time instrumentation inserts data-flow callbacks into kernel object files, (2) a runtime transport layer delivers records to user-space via a lock-free per-task ring buffer, and
2
Per-Task Data-Flow Extraction at Kernel Function Boundaries
Edge Coverage (KCOV)
+ Data-Flow Extraction
syscall()
syscall()
vfs_open()
vfs_open()
flags=?
A P REPRINT
flags=O_WRONLY|O_TRUNC
do_open()
do_open()
Sees: A→B→C
Sees: A→B(flags=0x241)→C
Figure 1: Edge coverage reports identical paths for different argument values. Data-flow extraction reveals the securityrelevant difference. Compile Time
PR
LLVM SanCov Pass (dataflow-args/ret)
inserts callbacks
Kernel Runtime
__sanitizer_cov _trace_args/ret
atomic write
Per-Task Ring Buffer
shared memory
User Space
mmap() consumer (fuzzer / analyst)
Figure 2: Three-layer architecture: compile-time instrumentation, kernel-resident lock-free buffer, and user-space consumption via mmap().
(3) user-space consumers (fuzzer executors or analyst tools) read the buffer via mmap(). The system is activated by the compiler flags -fsanitize-coverage=dataflow-args,dataflow-ret and requires no source-level annotation. 3.2
LLVM IR Boundary Instrumentation
The instrumentation pass resides in LLVM’s existing SanitizerCoverage framework (SanitizerCoverage.cpp). For each function F with a DISubprogram debug info attachment: Argument Capture.
At function entry, for each parameter i:
1. Resolve the parameter’s DILocalVariable → DIType. 2. If the type is a DICompositeType (struct/union): walk its DIDerivedType members to extract field byteoffsets and sizes. Compute an FNV-1a hash of the type name for deduplication. 3. Emit a compile-time constant global array: [hash, off0 , sz0 , off1 , sz1 , ..., offN , szN ]. 4. Insert a call to __sanitizer_cov_trace_args(pc, arg_idx, arg_size, ptr, offsets_ptr, num_fields) where offsets_ptr points past the hash element (GEP index 1).
For scalar arguments, offsets_ptr is null and num_fields is 0. Pointer arguments are passed directly; scalars are spilled to an alloca so the kernel callback receives a uniform pointer interface. Return Value Capture. Using LLVM’s EscapeEnumerator, the pass locates every ReturnInst in F and inserts a call to __sanitizer_cov_trace_ret(pc, ret_size, ptr, offsets_ptr, num_fields) before each return. 3
Per-Task Data-Flow Extraction at Kernel Function Boundaries
N fields
3-word header area[0] count
type|seq
A P REPRINT
PC
meta
field0
field1
...
u64
Figure 3: Ring buffer layout: area[0] holds the atomic write counter; each record has a 3-word header followed by field values. Edge Cases. Functions without debug info, variadic functions, and naked functions are skipped gracefully. The pass is controlled by two cl::opt flags: -sanitizer-coverage-dataflow-args and -sanitizer-coverage-dataflow-ret. 3.3
Lock-Free Per-Task Ring Buffer
PR
The kernel backend adds three fields to task_struct: kcov_df_area (pointer to u64 buffer), kcov_df_size (buffer capacity), and kcov_df_seq (monotonic sequence counter). Write Path. The callback implementation uses atomic_add to reserve a contiguous slot in the buffer. If the reserved offset exceeds kcov_df_size, the event is silently dropped—no blocking, no allocation, no spinlock. All pointer dereferences use copy_from_kernel_nofault() to safely handle unmapped or poisoned memory (e.g., KASAN red zones, freed slabs). Values in the IS_ERR_VALUE range ([−4095, −1]) are detected and skipped. Record Format. Each record is a variable-length sequence of u64 words starting with a 3-word header: [type_and_seq | pc | meta | field_val0 | ...| field_valN ] (or a single scalar value if num_fields is 0). Here, type_and_seq packs the type marker (0xE0000000 for entry, 0xF0000000 for return) and the 24-bit sequence number. The meta word packs the 8-bit argument index (for entry, 0 for return), 8-bit argument or return size in bytes, and the lower 48 bits of the raw pointer value. Safety. Callbacks are marked notrace, __no_sanitize_coverage, and noinline to prevent recursion. No printk or allocation occurs in the data path. An in_task() guard at the top of the write path rejects all calls from non-process contexts (softirq, hardirq, and NMI), eliminating the risk of reentrant corruption when a hardware or software interrupt preempts a task that is already inside the callback. 3.4
Separate Device Interface
KCOV- DATAFLOW exposes /sys/kernel/debug/kcov_dataflow as a device completely independent from the legacy /sys/kernel/debug/kcov. This ensures zero interference with syzkaller’s existing KCOV usage.
The ioctl namespace uses magic ’d’:
• KCOV_DF_INIT_TRACE (0x80086401): allocate buffer • KCOV_DF_ENABLE (0x6464): start recording • KCOV_DF_DISABLE (0x6465): stop recording
User-space maps the buffer via mmap() for zero-copy reads. 3.5
Rust Module Support
The Rust compiler (rustc) bundles its own LLVM (v22 for rustc 1.95) which lacks our custom flags. We solve this with a post-compilation pipeline: 1. rustc –emit=llvm-ir: generate LLVM IR text with debug info 2. opt -passes=sancov-module -sanitizer-coverage-dataflow-args -sanitizer-coverage-dataflow-ret: instrument with our pass 3. llc -filetype=obj: compile to object code 4. Standard module linking produces the instrumented .ko
This works because LLVM IR is language-agnostic: our pass operates identically on Rust-generated IR. The text format (.ll) avoids bitcode version incompatibilities between LLVM 22 and 23. Struct field expansion works on Rust #[repr(C)] types whose DICompositeType metadata is preserved by rustc -g. 4
Per-Task Data-Flow Extraction at Kernel Function Boundaries
rustc –emit=llvm-ir Rust LLVM 22
.ll
opt (our pass)
.ll
Custom LLVM 23
llc -filetype=obj
.o
A P REPRINT
ld module.ko
Custom LLVM 23
Figure 4: Rust post-compilation pipeline: text-format IR avoids bitcode version incompatibility between rustc’s bundled LLVM and our custom pass. 3.6
Pointer Dereference and Dynamic Struct Capture
A key design question is whether KCOV- DATAFLOW can capture struct fields from dynamically allocated (heap) memory, not just stack-local values. The answer is yes: the kernel callback performs one level of pointer dereference at runtime.
PR
Mechanism. When a function parameter is a pointer to a struct (e.g., struct session_data *sd where sd was kmalloc’d), the LLVM pass emits the pointer value and the compile-time offset array. At runtime, the callback computes each field address as field _addr = ptr + offset[i] and reads via copy_from_kernel_nofault(). This works regardless of whether the struct resides on the heap, stack, or in static memory. Safety for edge cases:
• Freed slab (KASAN poison): Safely reads poison bytes (0xfd/0xff)—revealing the object was freed. • Unmapped page: copy_from_kernel_nofault() returns error; field stored as 0. • ERR_PTR: Detected via IS_ERR_VALUE range check; pointer not dereferenced. • NULL: Detected and skipped (no crash). • Nested structs: Flattened—DICompositeType includes nested fields with correct absolute offsets.
Design choice: shallow capture. We perform exactly one level of dereference (pointer → struct fields). We do not chase pointers inside the struct (e.g., a char *buf field stores the pointer value, not the string contents). This bounds the cost to O(n) where n is the number of struct fields, and avoids recursive faults on invalid nested pointers. The pointer value itself is informative: NULL indicates freed/uninitialized, 0xffff888... indicates valid kernel heap, and KASAN poison patterns indicate use-after-free.
4
Implementation
4.1
LLVM Pass (600 LOC)
The pass extends SanitizerCoverage.cpp with two new methods:
InjectTraceForArgs(Function &F) iterates the function’s formal parameters, resolving each to its DILocalVariable via the DISubprogram attached to F . For each parameter whose type resolves (through stripDITypedefs()) to a DICompositeType, the pass calls getStructFieldOffsets() which: (a) walks the composite’s member list extracting byte offsets and sizes, (b) computes an FNV-1a hash of the struct’s name (stored at index 0 of the offsets array for type deduplication), and (c) creates a module-level ConstantArray global.
InjectTraceForRet(Function &F) uses EscapeEnumerator to find all function exit points and inserts the returnvalue callback before each ReturnInst.
Integration into the clang driver required changes to: CodeGenOptions.def (two new CODEGENOPT flags), SanitizerArgs.cpp (parsing "dataflow-args"/"dataflow-ret" with enum values 1 ≪ 20 and 1 ≪ 21), BackendUtil.cpp (wiring to SanitizerCoverageOptions), and Instrumentation.h (adding DataflowArgs/DataflowRet booleans). 4.2
Kernel Backend (400 LOC)
The kernel implementation in kernel/kcov.c adds:
• struct kcov_dataflow: owns buffer, size, sequence counter, and file_operations. • __sanitizer_cov_trace_args(): marked notrace, reads struct copy_from_kernel_nofault(), writes the record using atomic slot reservation. • __sanitizer_cov_trace_ret(): same pattern for return values. 5
fields
via
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Kconfig options CONFIG_KCOV_DATAFLOW_ARGS and CONFIG_KCOV_DATAFLOW_RET (both depending on CONFIG_KCOV) gate compilation. 4.3
Build System Integration
scripts/Makefile.kcov exports CFLAGS_KCOV_DATAFLOW containing the scripts/Makefile.lib applies these flags when a module’s Makefile declares:
coverage
flags.
K C O V _ D A T A FL O W_ my _m o du le . o := y
Global Enablement. CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL mirrors CONFIG_KCOV_INSTRUMENT_ALL: when enabled, all kernel objects are compiled with dataflow instrumentation automatically. Individual files or directories can opt out with KCOV_DATAFLOW_file.o := n or KCOV_DATAFLOW := n. This enables whole-kernel argument/return extraction for comprehensive fuzzing campaigns. Optional Inlining Control. CONFIG_KCOV_DATAFLOW_NO_INLINE (default y) adds -fno-inline to instrumented files for complete argument visibility. Setting it to n allows global enablement with significantly lower overhead— inlined functions lose individual argument records but the kernel retains normal performance characteristics and stack depth.
PR
1
sanitizer
For Rust modules, the wrapper script tools/kcov-dataflow/rustc-kcov-dataflow.sh automates the three-stage pipeline. 4.4
User-Space Consumer
A minimal consumer opens /dev/kcov_dataflow, calls ioctl(KCOV_DF_INIT_TRACE, buf_size) to allocate the buffer, mmap()s it, enables recording with ioctl(KCOV_DF_ENABLE), triggers the target kernel path, then reads records directly from the mapped buffer before calling ioctl(KCOV_DF_DISABLE).
5
Evaluation
5.1
Experimental Setup
We evaluate KCOV- DATAFLOW on linux-next 7.1.0-rc5 (next-20260528) compiled with our custom clang/LLVM 23 (commit 05a4e8c, trunk plus the dataflow pass). Rust modules are compiled with rustc 1.98.0-nightly (202605-22), built against the same LLVM 23 to share the dataflow instrumentation passes. The kernel is configured with CONFIG_KASAN=y, CONFIG_KCOV=y, CONFIG_KCOV_DATAFLOW_ARGS=y, CONFIG_KCOV_DATAFLOW_RET=y, CONFIG_RUST=y, CONFIG_ANDROID_BINDER_IPC=y, and CONFIG_SAMPLE_RUST_MISC_DEVICE=y. Tests run under virtme-ng (QEMU/KVM) with 8 vCPUs and 1 GB RAM. Thirteen purpose-built kernel modules (10 Rust, 3 C) exercise distinct scenarios: FFI contract violation, silent in-bounds corruption, 10-level deep taint propagation, binder ioctl edge cases, and Rust core API boundary conditions (KVec, RBTree, Arc, Page, UserSlice, credential subsystem). Source code is available at: kernel1 , LLVM2 , and rustc3 . 5.2
Correctness Verification
Table 1 shows that KCOV- DATAFLOW correctly captures pre-corruption (ENTRY) and post-corruption (RET) argument values for all five cases, matching printk ground truth. 5.3
Information Gain Over Existing Tools
Table 2 compares the observability provided by KCOV- DATAFLOW against KASAN reports and drgn vmcore analysis. KASAN reports the address of a violation but not which argument carried the bad value or which caller passed it. drgn can read C locals from a vmcore but fails entirely on Rust frames compiled at -O2 (DWARF variable locations are elided). KCOV- DATAFLOW captures both C and Rust arguments at runtime regardless of optimization level. 1
https://github.com/yskzalloc/linux https://github.com/yskzalloc/llvm-project 3 https://github.com/yskzalloc/rust 2
6
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Table 1: Correctness verification across five vulnerability classes. Case
ENTRY Captured
RET Captured
drgn
KASAN
OOB Write (C) UAF Write (C) Double-Free (C) 10-Deep Chain (C) Rust Module
struct{id=0x1337, buf="initial_", size=15} struct{id=0, buf=poison(0xfd), size=16} struct{id=0, buf=poison(0xff), size=99} offset propagation: 16 → 48 → 24 → 8 struct{0xcafe, 0xdeadbeef.., 0xa}, mult=3, cookie=0x1111..
struct{id=0x1337, _, size=32} struct{0x41414141, "UAF_CORR", 0xdead} struct{0xdf00df00, "DOUBLEFR", 0xdf} commit_write(idx=8) OOB ret=0xefbed00023456789
✓ ✓ ✓ ✓ ✗
slab-out-of-bounds slab-use-after-free double-free slab-out-of-bounds N/A
Table 2: Information gain comparison. Observable
drgn
Ours
✗ ✗ ✗ ✗ N/A ✗
✓ (C) partial ✓ (C) manual ✗ ✗
✓ ✓ ✓ ✓ ✓ ✓
PR
Which argument corrupted Pre-corruption value Post-corruption value Cross-function propagation Rust module arguments No-crash semantic bugs
KASAN
5.4
Performance Overhead
We measure overhead using a micro-benchmark that triggers 8 instrumented functions (1–8 arguments each, 44 total callbacks per iteration) over 5,000 iterations in a QEMU/KVM guest, taking the best of 3 runs after warmup to eliminate cache effects. The per-call cost is 1.24 µs for 44 callbacks (8 functions × avg. 4.5 args + 8 returns), yielding ∼27 ns per individual callback. This cost is dominated by one LOCK XADD (atomic slot reservation) plus one copy_from_kernel_nofault() per struct field. Critically, non-instrumented code paths incur zero overhead—the compiler emits no callbacks for modules without the KCOV_DATAFLOW flag. Kernel boot time is unchanged. Global Enablement Overhead. With CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y and CONFIG_KCOV_DATAFLOW_NO_INLINE=n (inlining preserved), the kernel boots normally under virtme-ng. Table 4 quantifies the overhead when all kernel objects are instrumented but recording is not enabled (the common idle state).
The .data increase (+44%) is due to per-function __sancov_offsets_ constant arrays containing struct field layouts. The syscall overhead (+133%) stems from per-function prologue code (alloca spills, 6-argument call setup, branch on kcov_df_enabled) and increased icache pressure. For comparison, basic KCOV (trace-pc) adds only 5–10% with INSTRUMENT_ALL since it emits a single call per edge with no argument setup. Global enablement is suitable for targeted investigation sessions and short-lived fuzzing campaigns. For continuous fuzzing (e.g., multi-day syzkaller runs), per-module instrumentation remains recommended. Comparison with Kernel Sanitizers. Table 5 contextualizes KCOV- DATAFLOW’s overhead against established kernel sanitizers. All measurements include CONFIG_KASAN=y in the baseline (the standard fuzzing configuration).
The overhead of KCOV- DATAFLOW falls within the range of a single sanitizer (KASAN/KCSAN) and is well below KMSAN. Since fuzzing kernels routinely stack multiple sanitizers (KASAN + KCOV + UBSAN), adding dataflow extraction fits naturally within the accepted performance budget. Notably, our measurements already include KASAN— the +133% is the additional cost on top of an already-sanitized kernel. 5.5
Triage Capability
For each vulnerability case, the ENTRY/RET record provides: (1) the exact argument that was corrupted (identified by arg_idx and field offset), (2) the struct field where corruption occurred (visible as a value change between ENTRY and RET), and (3) the call chain leading to corruption (via sequence numbers and PC values). The 10-deep chain case demonstrates that KCOV- DATAFLOW captures taint propagation across function boundaries automatically, identifying the root cause (validate_header() missing bounds check) from the dataflow log alone. 7
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Table 3: Performance overhead of KCOV- DATAFLOW. Configuration No recording (baseline) KCOV- DATAFLOW recording ON
Latency
Overhead
15.0 µs/iter 16.3 µs/iter
— +8.3%
Table 4: Global instrumentation overhead (recording disabled). Metric vmlinux .text vmlinux .data bzImage Boot time Syscall latency
INSTRUMENT_ALL
Overhead
81.2 MB 13.1 MB 33.2 MB 9.3 s 20.5 µs/iter
88.9 MB 18.9 MB 37.6 MB 15.9 s 47.7 µs/iter
+9.5% +44% +13% +71% +133%
8-Argument Stress Test
PR
5.6
Baseline
To verify correct handling of functions exceeding the x86-64 register ABI (6 registers), we test with 8 arguments of mixed types including a struct pointer. The Rust module captures all 8 individually: seq =9 arg [0](8) seq =10 arg [1](4) seq =11 arg [2](8) seq =12 arg [3](4) seq =13 arg [4](8) seq =14 arg [5](4) seq =15 arg [6](8) seq =16 arg [7](8) seq =145 ret (8)
= = = = = = = = =
0 x1111111111111111 0 x22222222 0 x3333333333333333 0 x44444444 0 x5555555555555555 0 x66666666 struct {. x =0 xaaaa , . y =0 xbbbb } 0 x8888888888888888 0 x22222222eef05552
All values match printk ground truth. Struct field expansion works on arg[6] (pointer to struct pair). Stack-passed arguments (7–8) are captured correctly alongside register-passed arguments (1–6).
6
Real-World Vulnerability Discovery
We applied KCOV- DATAFLOW to production kernel code—the Android Binder driver (drivers/android/binder.c and its Rust rewrite) and Rust kernel core subsystems (rust/kernel/)—discovering 60 findings across 14 source files. None are detectable by KASAN, edge coverage, or drgn. 6.1
Methodology
For each target subsystem, we: (1) enable KCOV_DATAFLOW on the module, (2) write a user-space program exercising edge-case inputs, (3) capture the KCOV- DATAFLOW log, (4) analyze ENTRY/RET argument values for anomalies. No fuzzer is used—purely deterministic log auditing. Table 5: Overhead comparison with kernel sanitizers.
Tool
Syscall Overhead
Basic KCOV (trace-pc) KCOV + comparisons KCOV- DATAFLOW (INSTRUMENT_ALL) KASAN (generic) KCSAN KMSAN
+5–10% +15–30% +133% +100–200% +100–200% +200–400%
8
Instruments
1 call/edge edges + cmp operands per-arg + per-ret callbacks every memory access every memory access every uninitialized use
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Table 6: Critical and High severity findings from real kernel code. Severity
Location
Finding
Impact
High High Medium Medium Medium
rust/kernel/error.rs:441 drivers/android/binder.c drivers/android/binder/*.rs rust/kernel/uaccess.rs rust/kernel/rbtree.rs
from_errno(-4096) silently returns EINVAL SET_MAX_THREADS accepts any u32 BC_ENTER_LOOPER duplicate accepted Zero-length UserSlice read succeeds without validating ptr Duplicate key insert silently replaces
Masks real error codes; wrong code path taken 0xffffffff → unbounded thread creation → OOM Thread in inconsistent looper state Address never checked Callers expecting “fail if exists” get wrong behavior
Table 7: Audit results by category (60 test cases). Count
High Medium Low Info Novel
4 8 4 37 7
Meaning Exploitable bugs (OOB, NULL deref, DoS) Missing validation or semantic issues Minor correctness concerns Correct behavior confirmed (not bugs) First-ever Rust observability at -O2
Critical and High Findings
PR
6.2
Category
Error Masking (High). Error::from_errno(-4096) is outside the valid errno range [−1, −4095] but silently returns EINVAL. The source contains // TODO: Make it a WARN_ONCE. Any C function returning an unexpected error code gets misinterpreted by Rust callers.
Binder Thread Exhaustion (High). BINDER_SET_MAX_THREADS stores any u32 directly into proc->max_threads without validation. Setting 0xffffffff allows unbounded thread creation—a denial-of-service vector on Android where binder access is available to apps. 6.3
Rust Kernel Core Audit (30 Findings)
We audited rust/kernel/{alloc, sync, page, uaccess, fs, task, error, rbtree, str}.rs with edgecase inputs: • KVec: reserve(usize::MAX) correctly returns AllocError; remove(0) on empty returns Err (unlike std::Vec which panics); push_within_capacity on full vec returns the rejected value. • RBTree: Duplicate key insert silently replaces (differs from std::HashMap::try_insert which returns Err). • Arc: into_unique_or_drop with multiple refs correctly drops; refcount operations captured at -O2. • Page: Cross-page boundary reads correctly rejected (offset+len > 4096 → EINVAL). • UserSlice: NULL, kernel-range, and unmapped addresses all return EFAULT. Zero-length succeeds without validation (potential issue).
6.4
Rust Binder IPC Audit (10 Findings)
The production Rust Binder (drivers/android/binder/*.rs) was exercised via binder ioctls. Key finding: the Rust binder has a cross-process ioctl detection (“get_current_thread was called from the wrong process”) that does not exist in the C binder—a security improvement only visible through KCOV- DATAFLOW. 6.5
Findings Summary
Of the 60 test cases, only 16 (High+Medium+Low) represent actual bugs or issues. The remaining 44 are positive results: the Info category confirms that kernel APIs handle edge cases correctly (e.g., KVec::reserve(usize::MAX) returns AllocError, fget(0xFFFFFFFF) returns NULL). These are not bugs—they demonstrate that KCOV- DATAFLOW is equally valuable for building confidence in correct implementations as for finding defects.
7
Why ftrace Cannot Trace Rust Functions
A natural question is whether existing tracing infrastructure (ftrace, kprobes) could achieve similar results. The answer is no, for fundamental architectural reasons. 9
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Table 8: Comparison: ftrace vs. KCOV- DATAFLOW for Rust kernel code. Capability
ftrace
KCOV- DATAFLOW
✗ ✗ ✗ ✓ ✓ ✗
✓ ✓ ✓ ✓ ✓ ✓
Trace Rust functions Capture argument values Struct field decomposition Zero source modification Works at -O2 Per-task isolation
ftrace requires -mfentry. The C compiler emits a call __fentry__ prologue when passed -pg -mfentry. The kernel’s build system passes this flag to clang/gcc for C files. However, rustc does not understand -mfentry—it has its own codegen pipeline. No __fentry__ call is emitted in Rust object files, so ftrace has nothing to patch at runtime.
PR
ftrace captures no arguments. Even for C functions where ftrace works, it only reports “function X was entered/exited.” Obtaining argument values requires writing a custom kprobe handler or eBPF program that reads specific registers—which requires knowing the exact calling convention and register assignment for each function. Rust symbol mangling. Rust function names are mangled into unreadable strings (e.g., _RNvMs3_NtCsgHKxaxYGgF6_11rust_binder...). Setting ftrace filters or kprobe targets requires these exact names. KCOV- DATAFLOW operates at LLVM IR level. Our pass runs on LLVM IR before codegen—the common representation shared by both clang (C) and rustc (Rust). At this level, function arguments are explicit SSA values with debug metadata, struct types have DICompositeType annotations, and the optimizer has not yet destroyed variable locations. The inserted callbacks survive optimization because they are function calls with side effects.
8
Boundary Contract Runtime Verification
We observe that KCOV- DATAFLOW enables a new form of runtime verification for kernel functions: verifying pre/postcondition contracts at function boundaries without source modification, for any function the monitored process executes—including functions the developer did not anticipate. 8.1
Functions as Black-Box Subsystems
Every kernel function call is a boundary between a caller (who provides inputs) and a callee (a black box from the caller’s perspective): [arg ,...,arg ]
[ret]
1 n Caller −−−− −−−−− → fcallee −−−→ Caller
The caller cannot observe what happens inside the callee. It can only choose arguments (precondition) and observe the return value (postcondition). KCOV- DATAFLOW captures both sides of this boundary, making the black box transparent without modifying it. 8.2
Formal Boundary Contracts
Define a Boundary Contract C(f ) = (P, Q) for function f :
• P(args): precondition predicate over input arguments • Q(args, ret): postcondition predicate over return value given inputs
Example: For binder_alloc_buf(alloc, data_size, offsets_size, is_async): P : data_size ≤ 220 ∧ offsets_size ≤ 216 Q : ret = 0 =⇒ alloc.buffer ̸= NULL
A contract violation occurs when the caller passes arguments violating P (caller bug) or the callee returns values violating Q given valid inputs (callee bug). 10
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
Table 9: Linux RV vs. KCOV- DATAFLOW contract verification. Dimension Scope Model Instrumentation Catches Unknown functions Rust support Specification
8.3
Linux RV
KCOV- DATAFLOW
Pre-defined events Automata on sequences Explicit tracepoints Ordering violations Cannot verify No Hardcoded in kernel
Any function executed Predicates on values Zero source modification Value violations Captures automatically Yes External database
The Unknown Kernel Path Problem
When a user-space process calls write(binder_fd, data, len), it triggers a chain of kernel functions:
PR
sys_write -> vfs_write -> binder_write -> b in d e r_ thread_write -> binder_transaction -> binder_alloc_buf -> ...
The user cannot predict which functions will execute. Linux RV requires pre-defining which events to monitor. KCOVDATAFLOW captures every function boundary automatically—the offline verifier then checks each captured (args, ret) pair against a contract database. 8.4
Comparison with Linux RV
Table 9 summarizes the differences. Linux RV verifies anticipated event orderings (e.g., “sleep must not occur in atomic context”). KCOV- DATAFLOW verifies arbitrary value contracts (e.g., “data_size must be ≤ 220 ”) on functions the developer never explicitly instrumented. 8.5
Concrete Example: Detecting the errno Bug
Contract for Error::from_errno(errno):
P : −4095 ≤ errno ≤ −1 Q : ret.code = errno
KCOV- DATAFLOW capture:
[ ENTRY ] pc = from_errno arg [0] = -4096 [ RET ] pc = from_errno ret = -22
// P violated // Q violated
Verifier output: precondition violated (−4096 ∈ / [−4095, −1]) and postcondition violated (ret = −22 ̸= −4096). Linux RV cannot catch this: no tracepoint exists inside from_errno, and it is a Rust function invisible to C-based tracing infrastructure. 8.6
Toward Post-Mortem Formal Verification
Classical formal verification proves properties for all inputs statically, but is intractable for 30M LOC kernels. We observe that KCOV- DATAFLOW’s structured output enables a complementary approach: post-mortem contract verification—mechanically checking recorded execution against formal specifications after the fact. Key Insight. The ring buffer produces a complete sequence of ⟨PC , args, ret⟩ tuples for every instrumented function the process executed. This is a finite execution trace in the formal methods sense—the input to a runtime verification monitor. What This Enables. 1. Offline model checking: Replay the buffer against temporal logic specifications (e.g., LTL: “every alloc is eventually followed by free with the same pointer”). 11
Per-Task Data-Flow Extraction at Kernel Function Boundaries
A P REPRINT
2. Contract refinement: Automatically infer likely contracts from many executions (Daikon-style invariant detection on kernel functions), then flag violations in new runs. 3. Differential verification: Compare argument/return value distributions across kernel versions to detect semantic regressions that pass all tests but violate implicit contracts. Soundness Boundary. This is not full formal verification: it only covers paths actually exercised. However, combined with a fuzzer that maximizes path diversity, it approaches bounded model checking—verifying contracts over all reachable states discovered during exploration. The structured, machine-readable output (unlike printk logs) makes this amenable to automated reasoning tools without manual log parsing.
9
Discussion & Limitations
Debug Info Requirement. Full struct field expansion requires CONFIG_DEBUG_INFO, -g; without it, only scalar argument values are captured. This is not a practical limitation for kernel fuzzing (KASAN already requires debug info).
PR
Rust Pipeline. The post-compilation pipeline is not yet integrated into Kbuild automatically; it requires a manual wrapper invocation. We propose adding native -Zsanitizer-coverage=dataflow-args support to rustc as future work. Buffer Overflow. If instrumented code generates more events than the buffer capacity, events are silently dropped. This is by design—blocking would introduce deadlocks in interrupt context. Interrupt and NMI Context. Instrumentation callbacks are restricted to process context via an in_task() check: any call originating from a softirq, hardirq, or NMI handler is silently discarded. This is stronger than the per-context filtering used by mainline KCOV (which allows softirq tracing via kcov_remote_start); we chose the stricter policy because the dataflow callback performs pointer dereferences (copy_from_kernel_nofault) whose latency is unacceptable in hard-interrupt paths, and because per-task buffer interleaving from nested contexts would corrupt record boundaries. Bugs triggered exclusively from interrupt context are therefore invisible to KCOV- DATAFLOW; however, such bugs are also unreachable by KCOV-guided fuzzers, which exercise kernel code through syscalls in process context. Fuzzer Integration (Future Work). Integrating KCOV- DATAFLOW with syzkaller requires modifying the executor to parse the mmap’d kcov_dataflow buffer and hash argument values into the coverage bitmap. We envision a composite feedback signal: unique (PC , arg_hash) pairs provide finer-grained state discrimination than unique edges alone. Upstream Path. We plan to submit the LLVM pass as an RFC to llvm-dev (SanitizerCoverage extension) and the kernel backend to [email protected] (KCOV maintainer: Dmitry Vyukov).
10
Related Work
Coverage-Guided Kernel Fuzzing. Syzkaller [1] and kAFL [6] use edge/block coverage as feedback. DIFUZE [7] adds interface-aware generation but no runtime data-flow. MORPHUZZ [8] fuzzes virtual devices without data-flow context. All rely solely on control-flow coverage.
Data-Flow Guided Fuzzing. Payer [9] argues for data-flow-guided fuzzing but provides no kernel implementation. Angora [3] uses byte-level taint for user-space programs at 10× overhead. GREYONE [10] combines taint with coverage but targets user-space only. None achieve the <5% overhead required for continuous kernel fuzzing. Kernel Instrumentation. KCOV [5] provides edge coverage—our foundation. ftrace/kprobes offer dynamic per-function tracing at high overhead. eBPF is programmable but restricted in context and cannot perform struct field expansion from debug metadata. Post-Mortem Analysis. drgn [11] enables programmable vmcore analysis but requires a crash and fails on optimized Rust code. kdump/crash provide full memory dumps but require manual analysis and miss silent bugs. KCOV- DATAFLOW is the first system combining compile-time data-flow instrumentation with a production-grade kernel transport, supporting both C and Rust with <3% overhead.
12
Per-Task Data-Flow Extraction at Kernel Function Boundaries
11
A P REPRINT
Conclusion
We presented KCOV- DATAFLOW, bridging the semantic gap between coverage-guided fuzzing and data-flow analysis for OS kernels. Our key insight is that function boundaries are natural observation points where data-flow context is both cheap to capture and highly informative for both automated exploration and human triage. The system requires no source annotation, operates safely in process context with explicit rejection of interrupt and NMI callbacks, and—uniquely—supports Rust kernel modules via a post-compilation pipeline that is the only known method for capturing Rust function arguments at runtime given -O2 DWARF elision. We demonstrated correctness on five vulnerability classes across C and Rust kernel modules, showing that boundary data-flow context provides information unavailable through any combination of KASAN, drgn, and edge coverage. Source code is available at https://github.com/yskzalloc/linux.
References [1] Dmitry Vyukov. syzkaller – kernel fuzzer. https://github.com/google/syzkaller, 2015.
PR
[2] Cornelius Aschermann, Sergej Schumilo, Ali Abbasi, and Thorsten Holz. IJON: Exploring deep state spaces via fuzzing. In IEEE S&P, 2020. [3] Peng Chen and Hao Chen. Angora: Efficient fuzzing by principled search. In IEEE S&P, 2018.
[4] Shuitao Gan, Chao Zhang, Xiaojun Qin, Xuwen Tu, Kang Li, Zhongyu Pei, and Zuoning Chen. CollAFL: Path sensitive fuzzing. In IEEE S&P, 2018. [5] Dmitry Vyukov. KCOV: Code coverage for fuzzing. Linux Kernel Documentation, 2016.
[6] Sergej Schumilo, Cornelius Aschermann, Robert Gawlik, Sebastian Schinzel, and Thorsten Holz. kAFL: Hardwareassisted feedback fuzzing for OS kernels. In USENIX Security, 2017. [7] Jake Corina, Aravind Machiry, Christopher Salls, Yan Shoshitaishvili, Shuang Hao, Christopher Kruegel, and Giovanni Vigna. DIFUZE: Interface aware fuzzing for kernel drivers. In ACM CCS, 2017. [8] Alexander Bulekov, Bandan Das, Stefan Hajnoczi, and Manuel Egele. MORPHUZZ: Bending (input) space to fuzz virtual devices. In USENIX Security, 2022. [9] Mathias Payer. The case for data-flow-guided fuzzing. In FEAST Workshop, 2019.
[10] Shuitao Gan, Chao Zhang, Peng Chen, Bodong Zhao, Xiaojun Qin, Dong Wu, and Zuoning Chen. GREYONE: Data flow sensitive fuzzing. In USENIX Security, 2020. [11] Omar Sandoval. drgn: Programmable debugger. https://github.com/osandov/drgn, 2019.
A
LLVM Pass: InjectTraceForArgs Pseudocode
1: function I NJECT T RACE F OR A RGS(F ) 2: DI ← F.getSubprogram() 3: if DI = null then return 4: end if 5: for each parameter Pi of F do 6: T ← stripTypedefs(DI .getType(i)) 7: if T is DICompositeType then 8: offsets ← getStructFieldOffsets(T ) 9: hash ← FNV1a(T.getName()) 10: Create global: [hash, off 0 , sz 0 , . . .] 11: else 12: offsets ← null; nfields ← 0 13: end if 14: ptr ← spill Pi to alloca if scalar, else use directly 15: Insert call: trace_args(pc, i, sizeof(Pi ), ptr, offsets, nfields) 16: end for 17: end function
13
Per-Task Data-Flow Extraction at Kernel Function Boundaries
B
A P REPRINT
Ring Buffer Record Format
Each record occupies 3 + N quadwords (64-bit each), where N is the number of fields (or 1 for scalar values when N = 0): Offset 0 1 2 3..3+N
C 1 2 3
Field type_and_seq pc meta field_val[0..N]
Description Type marker (bits [31:28]: 0xE=entry, 0xF=return) | seq (bits [23:0]) Instrumented function address arg_idx [63:56] | size [55:48] | pointer [47:0] Field or scalar values via copy_from_kernel_nofault
Rust Post-Compilation Pipeline
# 1. Emit LLVM IR with debug info rustc -- edition 2021 -- emit = llvm - ir -g \ -o module . ll module . rs -- crate - type lib
4 5
# 2. Instrument with our opt ( uses custom LLVM 23) opt - passes = sancov - module \ - sanitizer - coverage - level =3 \ - sanitizer - coverage - dataflow - args \ - sanitizer - coverage - dataflow - ret \ -S module . ll -o module_inst . ll
PR
6 7 8 9
10 11 12 13 14
# 3. Compile to object llc - filetype = obj - relocation - model = static \ - code - model = kernel module_inst . ll -o module . o
15 16
# 4. Link as normal kernel module
Listing 1: Rust module instrumentation pipeline
D
1 2 3 4 5 6
Ioctl Interface Specification
# define KCOV_DF_INIT_TRACE /* = 0 x80086401 */ # define KCOV_DF_ENABLE /* = 0 x6464 */ # define KCOV_DF_DISABLE /* = 0 x6465 */
_IOW ( ’d ’ , 1 , unsigned long )
_IO ( ’d ’ , 100)
_IO ( ’d ’ , 101)
Listing 2: Ioctl definitions
14