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 --- .../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 +#include +#include + +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