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 <[email protected]>
---
.../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