[PATCH RFC v3 4/8] selftests/kcov_dataflow: add rust_ffi_contract selftest

0 views
Skip to first unread message

Yunseong Kim

unread,
Sep 2, 2026, 12:30:08 PM (8 days ago) Sep 2
to linux-...@vger.kernel.org, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, rust-fo...@vger.kernel.org, ll...@lists.linux.dev, work...@vger.kernel.org, linu...@vger.kernel.org, linux-k...@vger.kernel.org, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Kees Cook, Nathan Chancellor, Nicolas Schier, Josh Poimboeuf, Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein, Alexandre Courbot, Onur Özkan, Nick Desaulniers, Bill Wendling, Marco Elver, Nikita Popov, Matt Arsenault, Justin Stitt, Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan, Yeoreum Yun, Yunseong Kim, Yunseong Kim
Add a test module that shows kcov_dataflow detecting a function-boundary
contract violation that leaves no crash and no KASAN report.

ffi_alloc_buf() has the postcondition "returns 0 implies out->buffer is
valid", but its async path with an empty pool returns 0 while leaving
out->buffer == NULL. The caller, ffi_check_result(), trusts the contract
and dereferences the buffer. Because kcov_dataflow captures the struct
fields at both boundaries, the violation is visible in the record stream:

0x0 = ffi_alloc_buf({0x0, 0x0, 0x0, 0x0}, 0x100, 0x10, 0x1)
0xfffffff2 = ffi_check_result({0x0, 0x110, 0x0, 0x0})
^ buffer still NULL after a 0 return

The module is opted into instrumentation with
KCOV_DATAFLOW_rust_ffi_contract.o := y and driven through a debugfs
trigger file; kselftest script will check the expanded struct at each
boundary, the scalar arguments (256, 16, 1) and the two return values.

Assisted-by: Claude:claude-opus-4-6 [kiro-chat]
Signed-off-by: Yunseong Kim <yunseo...@est.tech>
---
.../kcov_dataflow/rust_ffi_contract/Makefile | 3 +
.../kcov_dataflow/rust_ffi_contract/README.rst | 13 +++
.../rust_ffi_contract/rust_ffi_contract.c | 125 +++++++++++++++++++++
3 files changed, 141 insertions(+)

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);

--
2.47.3

Reply all
Reply to author
Forward
0 new messages