[RFC PATCH v2 12/14] selftests/kcov_dataflow: add rust_ffi_contract test module

1 view
Skip to first unread message

Yunseong Kim

unread,
Jun 11, 2026, 12:22:04 PMJun 11
to Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Andrew Morton, Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich, Nathan Chancellor, Nicolas Schier, Nick Desaulniers, Bill Wendling, Justin Stitt, Kees Cook, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Shuah Khan, Jonathan Corbet, Shuah Khan, Yunseong Kim, linux-...@vger.kernel.org, kasa...@googlegroups.com, rust-fo...@vger.kernel.org, linux-...@vger.kernel.org, ll...@lists.linux.dev, linu...@kvack.org, linux-k...@vger.kernel.org, work...@vger.kernel.org, linu...@vger.kernel.org, Yeoreum Yun
Demonstrates FFI contract violation detection. A C callee returns
success (0) but leaves buffer=NULL, violating the postcondition
"ret==0 implies buffer!=NULL". kcov_dataflow captures struct fields
at the boundary proving the violation without a crash or KASAN report.

Test:

make LLVM=1 CC=clang \
M=tools/testing/selftests/kcov_dataflow/rust_ffi_contract modules
vng --user root --exec \
"python3 tools/testing/selftests/kcov_dataflow/trigger-view.py \
rust_ffi_contract -C 8 --ko \
tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.ko"

Result:

vfs_write(0x0)
0x0 = full_proxy_write()
full_proxy_write(0x0, 0x1, 0x0)
0x8200080 = __debugfs_file_get()
__debugfs_file_get(0x0)
0x0 = __debugfs_file_get()
0x0 = rust_ffi_trigger_write [rust_ffi_contract]()
rust_ffi_trigger_write [rust_ffi_contract](0x0, 0x1, 0x0)
ffi_alloc_buf [rust_ffi_contract](0xffffffff912288ad, 0x100, 0x0, 0x1)
0x0 = ffi_alloc_buf [rust_ffi_contract]()
_printk(0x6f635f6966663601)
vprintk(0x6f635f6966663601, 0x8)
vprintk_default(0x6f635f6966663601, 0x8)
vprintk_emit(0x0, 0xffffffff, 0x0)
0x0 = panic_on_this_cpu()
0x0 = _prb_read_valid()
0x0 = prb_read_valid()
0x0 = console_unlock()
0x3f = vprintk_emit()
0x3f = vprintk_default()
0x3f = vprintk()
0x3f = _printk()
ffi_check_result [rust_ffi_contract](0x0)
_printk(0x6f635f6966663301)
vprintk(0x6f635f6966663301, 0x8)
vprintk_default(0x6f635f6966663301, 0x8)
vprintk_emit(0x0, 0xffffffff, 0x0)
0x0 = panic_on_this_cpu()
0x0 = _prb_read_valid()
0x0 = prb_read_valid()
0x0 = console_unlock()
0x3f = vprintk_emit()
0x3f = vprintk_default()
0x3f = vprintk()
0x3f = _printk()
0xfffffff2 = ffi_check_result [rust_ffi_contract]()
0x1 = rust_ffi_trigger_write [rust_ffi_contract]()
0x1 = full_proxy_write()
0x1 = vfs_write()
0x1 = ksys_write()
0x1 = __x64_sys_write()
0x0 = fpregs_assert_state_consistent()
0xba5748 = __x64_sys_close()
file_close_fd(0x4)
0x0 = file_close_fd()

Cc: Alexander Potapenko <gli...@google.com>
Assisted-by: Claude:claude-opus-4-6 [kiro-chat]
Link: https://github.com/yskzalloc/kcov-dataflow/actions
Signed-off-by: Yunseong Kim <yunseo...@est.tech>
---
tools/testing/selftests/kcov_dataflow/Makefile | 2 +-
tools/testing/selftests/kcov_dataflow/README.rst | 8 ++
.../kcov_dataflow/run_rust_ffi_contract.sh | 35 +++++++
.../kcov_dataflow/rust_ffi_contract/Makefile | 3 +
.../rust_ffi_contract/rust_ffi_contract.c | 111 +++++++++++++++++++++
5 files changed, 158 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/kcov_dataflow/Makefile b/tools/testing/selftests/kcov_dataflow/Makefile
index 3a42c54e954d..6412c90edfa1 100644
--- a/tools/testing/selftests/kcov_dataflow/Makefile
+++ b/tools/testing/selftests/kcov_dataflow/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
TEST_GEN_PROGS := user_ioctl/user_ioctl
-TEST_PROGS := run_eight_args_c.sh
+TEST_PROGS := run_eight_args_c.sh run_rust_ffi_contract.sh
include ../lib.mk
diff --git a/tools/testing/selftests/kcov_dataflow/README.rst b/tools/testing/selftests/kcov_dataflow/README.rst
index 61a41f3bd596..06a0c805cc74 100644
--- a/tools/testing/selftests/kcov_dataflow/README.rst
+++ b/tools/testing/selftests/kcov_dataflow/README.rst
@@ -48,3 +48,11 @@ eight_args_rust/

make LLVM=1 CC=clang M=tools/testing/selftests/kcov_dataflow/eight_args_rust modules
python3 trigger-view.py eight_args_rust
+
+rust_ffi_contract/
+ Demonstrates FFI contract violation detection. A callee returns
+ success but leaves buffer=NULL. kcov_dataflow captures struct
+ fields proving the violation::
+
+ make LLVM=1 CC=clang M=tools/testing/selftests/kcov_dataflow/rust_ffi_contract modules
+ python3 trigger-view.py rust_ffi_contract
diff --git a/tools/testing/selftests/kcov_dataflow/run_rust_ffi_contract.sh b/tools/testing/selftests/kcov_dataflow/run_rust_ffi_contract.sh
new file mode 100755
index 000000000000..8662e532296b
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/run_rust_ffi_contract.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Test rust_ffi_contract module capture via kcov_dataflow
+DIR="$(dirname "$0")"
+KO="$DIR/rust_ffi_contract/rust_ffi_contract.ko"
+
+if [ ! -f "$KO" ]; then
+ echo "SKIP: $KO not found"
+ echo "Build: make LLVM=1 CC=clang M=...rust_ffi_contract modules""
+ exit 4 # kselftest SKIP
+fi
+
+if [ ! -e /sys/kernel/debug/kcov_dataflow ]; then
+ echo "SKIP: kcov_dataflow not available"
+ exit 4
+fi
+
+OUTPUT=$(python3 "$DIR/trigger-view.py" rust_ffi_contract --ko "$KO" --raw 2>&1)
+RC=$?
+
+if [ $RC -ne 0 ]; then
+ echo "FAIL: trigger-and-view exited with $RC"
+ echo "$OUTPUT"
+ exit 1
+fi
+
+RECORDS=$(echo "$OUTPUT" | grep -c "^\[ENTRY\]\|^\[RET")
+if [ "$RECORDS" -gt 0 ]; then
+ echo "PASS: captured $RECORDS records from rust_ffi_contract"
+ exit 0
+else
+ echo "FAIL: no records captured"
+ echo "$OUTPUT"
+ exit 1
+fi
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 000000000000..d2a0261070b1
--- /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/rust_ffi_contract.c b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
new file mode 100644
index 000000000000..9cbb17c42195
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/rust_ffi_contract/rust_ffi_contract.c
@@ -0,0 +1,111 @@
+// 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, ...})
+ * ^ proves contract violated
+ *
+ * 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)
+{
+ if (!is_async) {
+ alloc->buffer = kmalloc(data_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;
+ return 0; /* contract violation */
+ }
+ alloc->buffer = kmalloc(data_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;
+
+ /* 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);

--
2.43.0

Reply all
Reply to author
Forward
0 new messages