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


Reply via email to