From 1e0587e1d36211d7ea3ba8bb9eb1305bc74bd013 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 18 Jun 2025 10:02:55 +0000 Subject: [PATCH 01/17] Add simple configuration for kftf module that is built with kernel into a .ko file. --- include/linux/kftf.h | 17 +++++++++++++++++ lib/Kconfig.debug | 1 + lib/Kconfig.kftf | 4 ++++ lib/Makefile | 2 ++ lib/kftf/Makefile | 4 ++++ lib/kftf/kftf_main.c | 11 +++++++++++ 6 files changed, 39 insertions(+) create mode 100644 include/linux/kftf.h create mode 100644 lib/Kconfig.kftf create mode 100644 lib/kftf/Makefile create mode 100644 lib/kftf/kftf_main.c diff --git a/include/linux/kftf.h b/include/linux/kftf.h new file mode 100644 index 00000000000000..ece1860d1ce393 --- /dev/null +++ b/include/linux/kftf.h @@ -0,0 +1,17 @@ +#ifndef KFTF_H +#define KFTF_H + +#include + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Ethan Graham "); +MODULE_DESCRIPTION("Kernel Fuzz Testing Framework"); + +struct foo { + int size; //< size of struct foo + int access; +}; + +void kftf_fuzzable(struct foo *foo); + +#endif /* KFTF_H */ diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index ebe33181b6e6e0..42cd345f93ad81 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -1040,6 +1040,7 @@ config MEM_ALLOC_PROFILING_DEBUG source "lib/Kconfig.kasan" source "lib/Kconfig.kfence" source "lib/Kconfig.kmsan" +source "lib/Kconfig.kftf" endmenu # "Memory Debugging" diff --git a/lib/Kconfig.kftf b/lib/Kconfig.kftf new file mode 100644 index 00000000000000..c948ba08ec3fe4 --- /dev/null +++ b/lib/Kconfig.kftf @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only + +config KFTF + tristate "Enable Kernel Fuzz Testing Framework (KFTF)" diff --git a/lib/Makefile b/lib/Makefile index c38582f187dd81..33e75b3682018e 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -354,6 +354,8 @@ obj-$(CONFIG_GENERIC_LIB_CMPDI2) += cmpdi2.o obj-$(CONFIG_GENERIC_LIB_UCMPDI2) += ucmpdi2.o obj-$(CONFIG_OBJAGG) += objagg.o +obj-$(CONFIG_KFTF) += kftf/ + # pldmfw library obj-$(CONFIG_PLDMFW) += pldmfw/ diff --git a/lib/kftf/Makefile b/lib/kftf/Makefile new file mode 100644 index 00000000000000..6e1df924ce37ae --- /dev/null +++ b/lib/kftf/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only + +obj-$(CONFIG_KFTF) += kftf.o +kftf-objs := kftf_main.o diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c new file mode 100644 index 00000000000000..dc9dc84f2a7518 --- /dev/null +++ b/lib/kftf/kftf_main.c @@ -0,0 +1,11 @@ +#include +#include + +// contains a bug! +void kftf_fuzzable(struct foo *foo) +{ + // can buffer overflow or underflow, or cause a null pointer dereference + // crashing the kernel + char *data = ((char *)foo) + foo->access; + printk("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); +} From 43dc89375f3c8b01b55552a9ac00d588a6b756c3 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 18 Jun 2025 10:18:06 +0000 Subject: [PATCH 02/17] kftf: Add init and exit functions to module --- lib/kftf/kftf_main.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index dc9dc84f2a7518..e6bce52a2e0657 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -7,5 +7,25 @@ void kftf_fuzzable(struct foo *foo) // can buffer overflow or underflow, or cause a null pointer dereference // crashing the kernel char *data = ((char *)foo) + foo->access; - printk("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); + pr_info("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); } + +static int __init kftf_init(void) +{ + struct foo foo = { + .access = -1, + .size = 0, + }; + + pr_info("%s: enter\n", __FUNCTION__); + kftf_fuzzable(&foo); + return 0; +} + +static void __exit kftf_exit(void) +{ + pr_info("%s: exiting\n", __FUNCTION__); +} + +module_init(kftf_init); +module_exit(kftf_exit); From 42d6acc17d47167db836e88a7d2363dd21d40a92 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 18 Jun 2025 13:12:28 +0000 Subject: [PATCH 03/17] kftf: simple sysfs that accepts a struct for a single function and the binary representation down. Correctly causes a KASAN repot when passing a buggy input to it. --- include/linux/kftf.h | 6 ++-- lib/kftf/kftf_main.c | 18 +++++----- lib/kftf/kftf_simple_fuzzer.h | 66 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 lib/kftf/kftf_simple_fuzzer.h diff --git a/include/linux/kftf.h b/include/linux/kftf.h index ece1860d1ce393..ddf8b8fd53a60e 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -7,11 +7,11 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework"); -struct foo { - int size; //< size of struct foo +struct kftf_simple_arg { + int data; int access; }; -void kftf_fuzzable(struct foo *foo); +void kftf_fuzzable(struct kftf_simple_arg *foo); #endif /* KFTF_H */ diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index e6bce52a2e0657..5de45c5fd4d887 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -1,30 +1,28 @@ +#include +#include #include #include +#include "kftf_simple_fuzzer.h" + // contains a bug! -void kftf_fuzzable(struct foo *foo) +void kftf_fuzzable(struct kftf_simple_arg *foo) { // can buffer overflow or underflow, or cause a null pointer dereference // crashing the kernel char *data = ((char *)foo) + foo->access; + pr_info("%s: foo->data = %d\n", __FUNCTION__, foo->data); pr_info("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); } static int __init kftf_init(void) { - struct foo foo = { - .access = -1, - .size = 0, - }; - - pr_info("%s: enter\n", __FUNCTION__); - kftf_fuzzable(&foo); - return 0; + return kftf_simple_fuzzer_init(); } static void __exit kftf_exit(void) { - pr_info("%s: exiting\n", __FUNCTION__); + kftf_simple_fuzzer_cleanup(); } module_init(kftf_init); diff --git a/lib/kftf/kftf_simple_fuzzer.h b/lib/kftf/kftf_simple_fuzzer.h new file mode 100644 index 00000000000000..da4fb3ab78bb36 --- /dev/null +++ b/lib/kftf/kftf_simple_fuzzer.h @@ -0,0 +1,66 @@ +#ifndef KFTF_SIMPLE_FUZZER_H +#define KFTF_SIMPLE_FUZZER_H + +#include +#include +#include + +struct kftf_simple_fuzzer_state { + struct file_operations fops; + struct dentry *kftf_dir; + struct dentry *input_file; + char buffer[128]; //< buffer for user input +}; + +static struct kftf_simple_fuzzer_state st; + +/** + * write callback for the simple fuzzer + */ +static ssize_t kftf_fuzz_write(struct file *filp, const char __user *buf, + size_t len, loff_t *off) +{ + if (len >= sizeof(st.buffer)) + return -EINVAL; + + if (simple_write_to_buffer(st.buffer, sizeof(st.buffer) - 1, off, buf, + len) < 0) + return -EFAULT; + + if (len != sizeof(struct kftf_simple_arg)) { + pr_warn("incorrect data size\n"); + return -EINVAL; + } + + // intrepret the binary contents of the buffer + struct kftf_simple_arg *fuzz_arg = (void *)st.buffer; + kftf_fuzzable(fuzz_arg); + return len; +} + +static int kftf_simple_fuzzer_init(void) +{ + st.kftf_dir = debugfs_create_dir("kftf", NULL); + if (!st.kftf_dir) + return 1; // TODO: proper errors + + st.fops = (struct file_operations){ + .owner = THIS_MODULE, + .write = kftf_fuzz_write, + }; + + st.input_file = + debugfs_create_file("input", 0222, st.kftf_dir, NULL, &st.fops); + + if (!st.input_file) + return 1; // TODO: proper errors + + return 0; +} + +static void kftf_simple_fuzzer_cleanup(void) +{ + debugfs_remove(st.kftf_dir); +} + +#endif /* KFTF_SIMPLE_FUZZER_H */ From 4a35a644674623580d70a58f8c2392d4342ffe5f Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 20 Jun 2025 09:55:20 +0000 Subject: [PATCH 04/17] kftf: allow tests with macro definitions in .kftf section of binary. Added support for this for x86 arch, but it is probably fragile because we aren't guarding with a #ifdef yet --- arch/x86/kernel/vmlinux.lds.S | 9 ++++ include/linux/kftf.h | 56 ++++++++++++++++++++-- lib/Kconfig.kftf | 1 + lib/kftf/kftf_main.c | 90 ++++++++++++++++++++++++++++++----- lib/kftf/kftf_simple_fuzzer.h | 26 ---------- lib/kftf/kftf_tests.h | 37 ++++++++++++++ lib/math/int_sqrt.c | 7 ++- 7 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 lib/kftf/kftf_tests.h diff --git a/arch/x86/kernel/vmlinux.lds.S b/arch/x86/kernel/vmlinux.lds.S index 4fa0be732af10f..3750ad14f26473 100644 --- a/arch/x86/kernel/vmlinux.lds.S +++ b/arch/x86/kernel/vmlinux.lds.S @@ -112,6 +112,13 @@ ASSERT(__relocate_kernel_end - __relocate_kernel_start <= KEXEC_CONTROL_CODE_MAX #else #define KEXEC_RELOCATE_KERNEL #endif + +#define KFTF_TABLE \ + . = ALIGN(0X100); \ + __kftf_start = .; \ + KEEP(*(.kftf)); /* keep everything referencing section */ \ + __kftf_end = .; + PHDRS { text PT_LOAD FLAGS(5); /* R_E */ data PT_LOAD FLAGS(6); /* RW_ */ @@ -199,6 +206,8 @@ SECTIONS CONSTRUCTORS KEXEC_RELOCATE_KERNEL + KFTF_TABLE + /* rarely changed data like cpu maps */ READ_MOSTLY_DATA(INTERNODE_CACHE_BYTES) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index ddf8b8fd53a60e..320c4a3b54f036 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -5,13 +5,59 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); -MODULE_DESCRIPTION("Kernel Fuzz Testing Framework"); +MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFTF)"); -struct kftf_simple_arg { - int data; - int access; +struct kftf_test_case { + const char *name; + ssize_t (*write_callback)(struct file *filp, const char __user *buf, + size_t len, loff_t *off); }; -void kftf_fuzzable(struct kftf_simple_arg *foo); +/** + * Shared buffer used for copying data out of userspace + */ +static char kftf_input_buf[128]; + +/** + * Expected usage: + * + * ``` + * FUZZ_TEST(func, func_arg_type) + * ret = func(arg.arg1, arg.arg2, ..., arg.argn); + * validate(ret); + * } + * ``` + * + * The created structure will be registered by the kftf module, which creates + * a debugfs entry for it. The write callback is such that it accepts valid + * struct instances as input. + */ +#define FUZZ_TEST(func, func_arg_type) \ + /* forward decl */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off); \ + const struct kftf_test_case __fuzz_test__##func \ + __attribute__((__section__(".kftf"), __used__)) = { \ + .name = #func, \ + .write_callback = _write_callback_##func, \ + }; \ + /* user-defined write callback */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ + { \ + pr_info("invoke %s", __FUNCTION__); \ + if (len >= sizeof(kftf_input_buf)) \ + return -EINVAL; \ + if (simple_write_to_buffer((void *)kftf_input_buf, \ + sizeof(kftf_input_buf) - 1, off, \ + buf, len) < 0) \ + return -EFAULT; \ + if (len != sizeof(struct kftf_simple_arg)) { \ + pr_warn("incorrect data size\n"); \ + return -EINVAL; \ + } \ + func_arg_type *arg = (void *)kftf_input_buf; #endif /* KFTF_H */ diff --git a/lib/Kconfig.kftf b/lib/Kconfig.kftf index c948ba08ec3fe4..6a7d35a25b5f50 100644 --- a/lib/Kconfig.kftf +++ b/lib/Kconfig.kftf @@ -2,3 +2,4 @@ config KFTF tristate "Enable Kernel Fuzz Testing Framework (KFTF)" + depends on DEBUG_FS diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index 5de45c5fd4d887..6ad6259dca2af3 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -1,28 +1,94 @@ +#include "kftf_tests.h" #include #include #include #include -#include "kftf_simple_fuzzer.h" +extern const struct kftf_test_case __kftf_start[]; +extern const struct kftf_test_case __kftf_end[]; -// contains a bug! -void kftf_fuzzable(struct kftf_simple_arg *foo) -{ - // can buffer overflow or underflow, or cause a null pointer dereference - // crashing the kernel - char *data = ((char *)foo) + foo->access; - pr_info("%s: foo->data = %d\n", __FUNCTION__, foo->data); - pr_info("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); -} +struct kftf_debugfs_state { + struct file_operations fops; + struct dentry *test_dir; + struct dentry *input_file; +}; + +struct kftf_simple_fuzzer_state { + struct file_operations fops; + struct dentry *kftf_dir; + struct kftf_debugfs_state debugfs_state[1024]; // FIXME: fine for WIP +}; + +static struct kftf_simple_fuzzer_state st; + +// XXX: allows everyone to write - correct flags? +const umode_t kftf_debug_fs_flags = 0222; static int __init kftf_init(void) { - return kftf_simple_fuzzer_init(); + int ret = 0; + + st.kftf_dir = debugfs_create_dir("kftf", NULL); + if (!st.kftf_dir) { + pr_warn("kftf: could not create debugfs"); + return -ENODEV; + } + + if (IS_ERR(st.kftf_dir)) { + st.kftf_dir = NULL; + return PTR_ERR(st.kftf_dir); + } + + const struct kftf_test_case *test; + int i = 0; // XXX: find better way of doing this + for (test = __kftf_start; test < __kftf_end; test++) { + st.debugfs_state[i].test_dir = + debugfs_create_dir(test->name, st.kftf_dir); + + if (!st.debugfs_state[i].test_dir) { + ret = -ENOMEM; + goto cleanup_failure; + } else if (IS_ERR(st.debugfs_state[i].test_dir)) { + ret = PTR_ERR(st.debugfs_state[i].test_dir); + goto cleanup_failure; + } + + st.debugfs_state[i].fops = (struct file_operations){ + .owner = THIS_MODULE, + .write = test->write_callback, + }; + + st.debugfs_state[i].input_file = + debugfs_create_file("input", kftf_debug_fs_flags, + st.debugfs_state[i].test_dir, NULL, + &st.debugfs_state[i].fops); + + if (!st.debugfs_state[i].input_file) { + ret = -ENOMEM; + goto cleanup_failure; + } else if (IS_ERR(st.debugfs_state[i].input_file)) { + ret = PTR_ERR(st.debugfs_state[i].input_file); + goto cleanup_failure; + } + + i++; + pr_info("kftf: registered %s\n", test->name); + } + + return 0; + +cleanup_failure: + debugfs_remove_recursive(st.kftf_dir); + return ret; } static void __exit kftf_exit(void) { - kftf_simple_fuzzer_cleanup(); + if (!st.kftf_dir) + return; + + debugfs_remove_recursive(st.kftf_dir); + st.kftf_dir = NULL; } module_init(kftf_init); diff --git a/lib/kftf/kftf_simple_fuzzer.h b/lib/kftf/kftf_simple_fuzzer.h index da4fb3ab78bb36..ae99e6227c317f 100644 --- a/lib/kftf/kftf_simple_fuzzer.h +++ b/lib/kftf/kftf_simple_fuzzer.h @@ -8,7 +8,6 @@ struct kftf_simple_fuzzer_state { struct file_operations fops; struct dentry *kftf_dir; - struct dentry *input_file; char buffer[128]; //< buffer for user input }; @@ -38,29 +37,4 @@ static ssize_t kftf_fuzz_write(struct file *filp, const char __user *buf, return len; } -static int kftf_simple_fuzzer_init(void) -{ - st.kftf_dir = debugfs_create_dir("kftf", NULL); - if (!st.kftf_dir) - return 1; // TODO: proper errors - - st.fops = (struct file_operations){ - .owner = THIS_MODULE, - .write = kftf_fuzz_write, - }; - - st.input_file = - debugfs_create_file("input", 0222, st.kftf_dir, NULL, &st.fops); - - if (!st.input_file) - return 1; // TODO: proper errors - - return 0; -} - -static void kftf_simple_fuzzer_cleanup(void) -{ - debugfs_remove(st.kftf_dir); -} - #endif /* KFTF_SIMPLE_FUZZER_H */ diff --git a/lib/kftf/kftf_tests.h b/lib/kftf/kftf_tests.h new file mode 100644 index 00000000000000..c8e57677185254 --- /dev/null +++ b/lib/kftf/kftf_tests.h @@ -0,0 +1,37 @@ +#ifndef KFTF_TESTS_H +#define KFTF_TESTS_H + +#include +#include + +struct kftf_simple_arg { + int data; + int access; +}; + +// contains a bug! +static void kftf_fuzzable(struct kftf_simple_arg *foo) +{ + // can buffer overflow or underflow, or cause a null pointer dereference + // crashing the kernel + char *data = ((char *)foo) + foo->access; + pr_info("%s: foo->data = %d\n", __FUNCTION__, foo->data); + pr_info("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); +} + +FUZZ_TEST(kftf_fuzzable, struct kftf_simple_arg) +kftf_fuzzable(arg); +return len; +} + +struct int_sqrt_arg { + unsigned long x; +}; + +FUZZ_TEST(int_sqrt, struct int_sqrt_arg) +unsigned long res = int_sqrt(arg->x); +pr_info("fuzz_arg.x = %lu, res = %lu", arg->x, res); +return len; +} + +#endif /* KFTF_TESTS_H */ diff --git a/lib/math/int_sqrt.c b/lib/math/int_sqrt.c index a8170bb9142f39..1ce96d3792a40c 100644 --- a/lib/math/int_sqrt.c +++ b/lib/math/int_sqrt.c @@ -21,6 +21,11 @@ unsigned long int_sqrt(unsigned long x) { unsigned long b, m, y = 0; + // deliberate bug + if (x == 0) { + char c = *(char *)x; + pr_info("should crash here! %c\n", c); + } if (x <= 1) return x; @@ -51,7 +56,7 @@ u32 int_sqrt64(u64 x) u64 b, m, y = 0; if (x <= ULONG_MAX) - return int_sqrt((unsigned long) x); + return int_sqrt((unsigned long)x); m = 1ULL << ((fls64(x) - 1) & ~1ULL); while (m != 0) { From e8992cb9d4c89b46957cb5d8503730805806cfd4 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 12:31:45 +0000 Subject: [PATCH 05/17] kftf: fixx sizeof check so that it checks against the provided type. --- include/linux/kftf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 320c4a3b54f036..37844322813e69 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -54,7 +54,7 @@ static char kftf_input_buf[128]; sizeof(kftf_input_buf) - 1, off, \ buf, len) < 0) \ return -EFAULT; \ - if (len != sizeof(struct kftf_simple_arg)) { \ + if (len != sizeof(func_arg_type)) { \ pr_warn("incorrect data size\n"); \ return -EINVAL; \ } \ From d7adb902c22e0245ac3d737858b2c2b74e0e7f18 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 13:45:01 +0000 Subject: [PATCH 06/17] kftf: add metadata file per fuzzed function - this contains the name of the argument type that the function takes so that the user can use tools like pahole to generate a syzkaller definition of the argument and thus fuzz the function more easily. --- include/linux/kftf.h | 83 ++++++++++++++++++++++++++++++-------------- lib/kftf/kftf_main.c | 70 ++++++++++++++++++++++++++++--------- 2 files changed, 110 insertions(+), 43 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 37844322813e69..951f531d5e6370 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -9,8 +9,11 @@ MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFTF)"); struct kftf_test_case { const char *name; - ssize_t (*write_callback)(struct file *filp, const char __user *buf, + const char *arg_type_name; + ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, size_t len, loff_t *off); + ssize_t (*read_metadata_cb)(struct file *, char __user *, size_t, + loff_t *); }; /** @@ -32,32 +35,58 @@ static char kftf_input_buf[128]; * a debugfs entry for it. The write callback is such that it accepts valid * struct instances as input. */ -#define FUZZ_TEST(func, func_arg_type) \ - /* forward decl */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off); \ - const struct kftf_test_case __fuzz_test__##func \ - __attribute__((__section__(".kftf"), __used__)) = { \ - .name = #func, \ - .write_callback = _write_callback_##func, \ - }; \ - /* user-defined write callback */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off) \ - { \ - pr_info("invoke %s", __FUNCTION__); \ - if (len >= sizeof(kftf_input_buf)) \ - return -EINVAL; \ - if (simple_write_to_buffer((void *)kftf_input_buf, \ - sizeof(kftf_input_buf) - 1, off, \ - buf, len) < 0) \ - return -EFAULT; \ - if (len != sizeof(func_arg_type)) { \ - pr_warn("incorrect data size\n"); \ - return -EINVAL; \ - } \ +#define FUZZ_TEST(func, func_arg_type) \ + /* forward decls */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off); \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off); \ + /* test case struct initialization */ \ + const struct kftf_test_case __fuzz_test__##func \ + __attribute__((__section__(".kftf"), __used__)) = { \ + .name = #func, \ + .arg_type_name = #func_arg_type, \ + .write_input_cb = _write_callback_##func, \ + .read_metadata_cb = _read_metadata_callback_##func \ + }; \ + /* callback that simply returns the type name to the user */ \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off) \ + { \ + int bytes_to_copy; \ + int message_len = strlen(__fuzz_test__##func.arg_type_name); \ + if (*off >= message_len) { \ + return -EINVAL; \ + } \ + bytes_to_copy = message_len - *off; \ + if (bytes_to_copy > len) { \ + bytes_to_copy = len; \ + } \ + if (copy_to_user(buf, \ + __fuzz_test__##func.arg_type_name + *off, \ + bytes_to_copy) != 0) { \ + return -EFAULT; \ + } \ + *off += bytes_to_copy; \ + return bytes_to_copy; \ + } \ + /* user-defined write callback */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ + { \ + pr_info("invoke %s", __FUNCTION__); \ + if (len >= sizeof(kftf_input_buf)) \ + return -EINVAL; \ + if (simple_write_to_buffer((void *)kftf_input_buf, \ + sizeof(kftf_input_buf) - 1, off, \ + buf, len) < 0) \ + return -EFAULT; \ + if (len != sizeof(func_arg_type)) { \ + pr_warn("incorrect data size\n"); \ + return -EINVAL; \ + } \ func_arg_type *arg = (void *)kftf_input_buf; #endif /* KFTF_H */ diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index 6ad6259dca2af3..df9f82de734be9 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -7,27 +7,46 @@ extern const struct kftf_test_case __kftf_start[]; extern const struct kftf_test_case __kftf_end[]; -struct kftf_debugfs_state { +#define KFTF_MAX_TEST_CASES 1024 + +/// defines a dentry with file-operations +struct kftf_dentry { + struct dentry *dentry; struct file_operations fops; +}; + +/** + * Wraps teh state of the created + */ +struct kftf_debugfs_state { struct dentry *test_dir; - struct dentry *input_file; + struct kftf_dentry input_dentry; + struct kftf_dentry metadata_dentry; }; struct kftf_simple_fuzzer_state { struct file_operations fops; struct dentry *kftf_dir; - struct kftf_debugfs_state debugfs_state[1024]; // FIXME: fine for WIP + struct kftf_debugfs_state + debugfs_state[KFTF_MAX_TEST_CASES]; // FIXME: fine for WIP }; static struct kftf_simple_fuzzer_state st; -// XXX: allows everyone to write - correct flags? -const umode_t kftf_debug_fs_flags = 0222; +// XXX: be careful of flags here +const umode_t kftf_flags_w = 0666; +const umode_t kftf_flags_r = 0444; static int __init kftf_init(void) { int ret = 0; + // To avoid kmalloc entirely, we enforce a maximum number of fuzz tests + // that can be defined inside the kernel. + size_t num_test_cases = __kftf_end - __kftf_start; + if (num_test_cases > KFTF_MAX_TEST_CASES) + return -EINVAL; + st.kftf_dir = debugfs_create_dir("kftf", NULL); if (!st.kftf_dir) { pr_warn("kftf: could not create debugfs"); @@ -53,21 +72,40 @@ static int __init kftf_init(void) goto cleanup_failure; } - st.debugfs_state[i].fops = (struct file_operations){ - .owner = THIS_MODULE, - .write = test->write_callback, - }; + st.debugfs_state[i].input_dentry.fops = + (struct file_operations){ + .owner = THIS_MODULE, + .write = test->write_input_cb, + }; - st.debugfs_state[i].input_file = - debugfs_create_file("input", kftf_debug_fs_flags, - st.debugfs_state[i].test_dir, NULL, - &st.debugfs_state[i].fops); + st.debugfs_state[i].input_dentry.dentry = debugfs_create_file( + "input", kftf_flags_w, st.debugfs_state[i].test_dir, + NULL, &st.debugfs_state[i].input_dentry.fops); + if (!st.debugfs_state[i].input_dentry.dentry) { + ret = -ENOMEM; + goto cleanup_failure; + } else if (IS_ERR(st.debugfs_state[i].input_dentry.dentry)) { + ret = PTR_ERR(st.debugfs_state[i].input_dentry.dentry); + goto cleanup_failure; + } - if (!st.debugfs_state[i].input_file) { + st.debugfs_state[i].metadata_dentry.fops = + (struct file_operations){ + .owner = THIS_MODULE, + .read = test->read_metadata_cb, + }; + + st.debugfs_state[i].metadata_dentry.dentry = + debugfs_create_file( + "metadata", kftf_flags_r, + st.debugfs_state[i].test_dir, NULL, + &st.debugfs_state[i].metadata_dentry.fops); + if (!st.debugfs_state[i].metadata_dentry.dentry) { ret = -ENOMEM; goto cleanup_failure; - } else if (IS_ERR(st.debugfs_state[i].input_file)) { - ret = PTR_ERR(st.debugfs_state[i].input_file); + } else if (IS_ERR(st.debugfs_state[i].metadata_dentry.dentry)) { + ret = PTR_ERR( + st.debugfs_state[i].metadata_dentry.dentry); goto cleanup_failure; } From ca5ba483c7e4be469ae2743bec2464b5d9f0d6cd Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 13:51:00 +0000 Subject: [PATCH 07/17] move int_sqrt test out of kftf_tests file and into the math source files. It works, and the function is exposed correctly! --- lib/kftf/kftf_tests.h | 21 ++++++--------------- lib/math/int_sqrt.c | 11 +++++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/kftf/kftf_tests.h b/lib/kftf/kftf_tests.h index c8e57677185254..b8121d04f9c191 100644 --- a/lib/kftf/kftf_tests.h +++ b/lib/kftf/kftf_tests.h @@ -10,27 +10,18 @@ struct kftf_simple_arg { }; // contains a bug! -static void kftf_fuzzable(struct kftf_simple_arg *foo) +static void kftf_fuzzable(int access, int data, void *ptr) { // can buffer overflow or underflow, or cause a null pointer dereference // crashing the kernel - char *data = ((char *)foo) + foo->access; - pr_info("%s: foo->data = %d\n", __FUNCTION__, foo->data); - pr_info("%s: data at foo[%d] = %x\n", __FUNCTION__, foo->access, *data); + char access_data = *(((char *)ptr) + access); + pr_info("%s: data = %0x\n", __FUNCTION__, data); + pr_info("%s: data at ptr[%d] = %x\n", __FUNCTION__, access, + access_data); } FUZZ_TEST(kftf_fuzzable, struct kftf_simple_arg) -kftf_fuzzable(arg); -return len; -} - -struct int_sqrt_arg { - unsigned long x; -}; - -FUZZ_TEST(int_sqrt, struct int_sqrt_arg) -unsigned long res = int_sqrt(arg->x); -pr_info("fuzz_arg.x = %lu, res = %lu", arg->x, res); +kftf_fuzzable(arg->access, arg->data, &arg); return len; } diff --git a/lib/math/int_sqrt.c b/lib/math/int_sqrt.c index 1ce96d3792a40c..cc9c59b655d2d0 100644 --- a/lib/math/int_sqrt.c +++ b/lib/math/int_sqrt.c @@ -10,6 +10,17 @@ #include #include #include +#include + +struct int_sqrt_arg { + unsigned long x; +}; + +FUZZ_TEST(int_sqrt, struct int_sqrt_arg) +unsigned long res = int_sqrt(arg->x); +pr_info("fuzz_arg.x = %lu, res = %lu", arg->x, res); +return len; +} /** * int_sqrt - computes the integer square root From 96ae8d557b734d05025e410c2db09c2fb4af1c57 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 16:25:20 +0000 Subject: [PATCH 08/17] kftf: address PR comments and make the FUZZ_TEST interface a lot more user friendly. --- include/linux/kftf.h | 16 +++++++++++----- lib/kftf/kftf_main.c | 33 +++++++++++++++++++-------------- lib/kftf/kftf_tests.h | 27 ++++++++++++++++++--------- lib/math/int_sqrt.c | 6 +++--- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 951f531d5e6370..13ba5d52b4c6cb 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -25,9 +25,9 @@ static char kftf_input_buf[128]; * Expected usage: * * ``` - * FUZZ_TEST(func, func_arg_type) - * ret = func(arg.arg1, arg.arg2, ..., arg.argn); - * validate(ret); + * FUZZ_TEST(func, func_arg_type) { + * ret = func(arg.arg1, arg.arg2, ..., arg.argn); + * validate(ret); * } * ``` * @@ -42,6 +42,7 @@ static char kftf_input_buf[128]; size_t len, loff_t *off); \ static ssize_t _read_metadata_callback_##func( \ struct file *filp, char __user *buf, size_t len, loff_t *off); \ + static void _fuzz_test_logic_##func(func_arg_type arg); \ /* test case struct initialization */ \ const struct kftf_test_case __fuzz_test__##func \ __attribute__((__section__(".kftf"), __used__)) = { \ @@ -76,7 +77,6 @@ static char kftf_input_buf[128]; const char __user *buf, \ size_t len, loff_t *off) \ { \ - pr_info("invoke %s", __FUNCTION__); \ if (len >= sizeof(kftf_input_buf)) \ return -EINVAL; \ if (simple_write_to_buffer((void *)kftf_input_buf, \ @@ -87,6 +87,12 @@ static char kftf_input_buf[128]; pr_warn("incorrect data size\n"); \ return -EINVAL; \ } \ - func_arg_type *arg = (void *)kftf_input_buf; + func_arg_type arg = *(func_arg_type *)kftf_input_buf; \ + /* call the user's logic on the provided arg. */ \ + /* NOTE: define some success/failure return types? */ \ + _fuzz_test_logic_##func(arg); \ + return len; \ + } \ + static void _fuzz_test_logic_##func(func_arg_type arg) #endif /* KFTF_H */ diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index df9f82de734be9..965370aed50dcf 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -1,15 +1,17 @@ -#include "kftf_tests.h" -#include #include -#include +#include #include +#include +#include "kftf_tests.h" extern const struct kftf_test_case __kftf_start[]; extern const struct kftf_test_case __kftf_end[]; #define KFTF_MAX_TEST_CASES 1024 -/// defines a dentry with file-operations +/** + * defines a struct dentry with file-operations + */ struct kftf_dentry { struct dentry *dentry; struct file_operations fops; @@ -33,17 +35,22 @@ struct kftf_simple_fuzzer_state { static struct kftf_simple_fuzzer_state st; -// XXX: be careful of flags here +/* XXX: Be careful of flags here. Should formally define what we want */ const umode_t kftf_flags_w = 0666; const umode_t kftf_flags_r = 0444; static int __init kftf_init(void) { + const struct kftf_test_case *test; int ret = 0; - - // To avoid kmalloc entirely, we enforce a maximum number of fuzz tests - // that can be defined inside the kernel. - size_t num_test_cases = __kftf_end - __kftf_start; + int i = 0; + size_t num_test_cases; + + /* + * To avoid kmalloc entirely, we enforce a maximum number of fuzz tests + * that can be defined inside the kernel. + */ + num_test_cases = __kftf_end - __kftf_start; if (num_test_cases > KFTF_MAX_TEST_CASES) return -EINVAL; @@ -58,9 +65,7 @@ static int __init kftf_init(void) return PTR_ERR(st.kftf_dir); } - const struct kftf_test_case *test; - int i = 0; // XXX: find better way of doing this - for (test = __kftf_start; test < __kftf_end; test++) { + for (test = __kftf_start; test < __kftf_end; test++, i++) { st.debugfs_state[i].test_dir = debugfs_create_dir(test->name, st.kftf_dir); @@ -72,12 +77,12 @@ static int __init kftf_init(void) goto cleanup_failure; } + /* create "input" file for fuzz test */ st.debugfs_state[i].input_dentry.fops = (struct file_operations){ .owner = THIS_MODULE, .write = test->write_input_cb, }; - st.debugfs_state[i].input_dentry.dentry = debugfs_create_file( "input", kftf_flags_w, st.debugfs_state[i].test_dir, NULL, &st.debugfs_state[i].input_dentry.fops); @@ -95,6 +100,7 @@ static int __init kftf_init(void) .read = test->read_metadata_cb, }; + /* create "metadata" file for fuzz test */ st.debugfs_state[i].metadata_dentry.dentry = debugfs_create_file( "metadata", kftf_flags_r, @@ -109,7 +115,6 @@ static int __init kftf_init(void) goto cleanup_failure; } - i++; pr_info("kftf: registered %s\n", test->name); } diff --git a/lib/kftf/kftf_tests.h b/lib/kftf/kftf_tests.h index b8121d04f9c191..b3895eebe5e7e3 100644 --- a/lib/kftf/kftf_tests.h +++ b/lib/kftf/kftf_tests.h @@ -5,24 +5,33 @@ #include struct kftf_simple_arg { - int data; - int access; + char first; + char second; + char third; }; // contains a bug! -static void kftf_fuzzable(int access, int data, void *ptr) +static void kftf_fuzzable(char first, char second, char third) { // can buffer overflow or underflow, or cause a null pointer dereference // crashing the kernel - char access_data = *(((char *)ptr) + access); - pr_info("%s: data = %0x\n", __FUNCTION__, data); - pr_info("%s: data at ptr[%d] = %x\n", __FUNCTION__, access, - access_data); + if (first == 'a') { + pr_info("first was a"); + if (second == 'b') { + pr_info("second was b"); + if (third == 'c') { + pr_info("third was c"); + /* do some weird access */ + char value = *(char *)&first + (first * 10 + 1); + pr_info("dumping ptr %c\b", value); + } + } + } } FUZZ_TEST(kftf_fuzzable, struct kftf_simple_arg) -kftf_fuzzable(arg->access, arg->data, &arg); -return len; +{ + kftf_fuzzable(arg.first, arg.second, arg.third); } #endif /* KFTF_TESTS_H */ diff --git a/lib/math/int_sqrt.c b/lib/math/int_sqrt.c index cc9c59b655d2d0..a52b16f8045393 100644 --- a/lib/math/int_sqrt.c +++ b/lib/math/int_sqrt.c @@ -17,9 +17,9 @@ struct int_sqrt_arg { }; FUZZ_TEST(int_sqrt, struct int_sqrt_arg) -unsigned long res = int_sqrt(arg->x); -pr_info("fuzz_arg.x = %lu, res = %lu", arg->x, res); -return len; +{ + unsigned long res = int_sqrt(arg.x); + pr_info("fuzz_arg.x = %lu, res = %lu", arg.x, res); } /** From ece3042da74cac1b97db823dfd0e729f9877b4e5 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 16:32:33 +0000 Subject: [PATCH 09/17] kftf: update include block so that the relative include (kftf_tests.h) has a line separator between it and the common linux includes, as per https://elixir.bootlin.com/linux/v6.12.5/source/mm/kfence/core.c --- lib/kftf/kftf_main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index 965370aed50dcf..e83382e7be6ce0 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -2,6 +2,7 @@ #include #include #include + #include "kftf_tests.h" extern const struct kftf_test_case __kftf_start[]; From 07bfafa4942a764d7b1d10f0a257f83f7c05cbfc Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 17:11:38 +0000 Subject: [PATCH 10/17] delete unused file kftf_simple_fuzzer --- lib/kftf/kftf_simple_fuzzer.h | 40 ----------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 lib/kftf/kftf_simple_fuzzer.h diff --git a/lib/kftf/kftf_simple_fuzzer.h b/lib/kftf/kftf_simple_fuzzer.h deleted file mode 100644 index ae99e6227c317f..00000000000000 --- a/lib/kftf/kftf_simple_fuzzer.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef KFTF_SIMPLE_FUZZER_H -#define KFTF_SIMPLE_FUZZER_H - -#include -#include -#include - -struct kftf_simple_fuzzer_state { - struct file_operations fops; - struct dentry *kftf_dir; - char buffer[128]; //< buffer for user input -}; - -static struct kftf_simple_fuzzer_state st; - -/** - * write callback for the simple fuzzer - */ -static ssize_t kftf_fuzz_write(struct file *filp, const char __user *buf, - size_t len, loff_t *off) -{ - if (len >= sizeof(st.buffer)) - return -EINVAL; - - if (simple_write_to_buffer(st.buffer, sizeof(st.buffer) - 1, off, buf, - len) < 0) - return -EFAULT; - - if (len != sizeof(struct kftf_simple_arg)) { - pr_warn("incorrect data size\n"); - return -EINVAL; - } - - // intrepret the binary contents of the buffer - struct kftf_simple_arg *fuzz_arg = (void *)st.buffer; - kftf_fuzzable(fuzz_arg); - return len; -} - -#endif /* KFTF_SIMPLE_FUZZER_H */ From 0c5cd7efce248fe929cf68c4466db88fd8e02132 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 17:12:05 +0000 Subject: [PATCH 11/17] kftf: per test-case input buffer with mutex --- include/linux/kftf.h | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 13ba5d52b4c6cb..80ede23d275d53 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -16,11 +16,6 @@ struct kftf_test_case { loff_t *); }; -/** - * Shared buffer used for copying data out of userspace - */ -static char kftf_input_buf[128]; - /** * Expected usage: * @@ -36,6 +31,10 @@ static char kftf_input_buf[128]; * struct instances as input. */ #define FUZZ_TEST(func, func_arg_type) \ + /* input buffer. Size 1 for now, but we may support batching */ \ + static func_arg_type input_buf_##func[2]; \ + /* guard the buffer as concurrent processes could race */ \ + DEFINE_MUTEX(input_mutex_##func); \ /* forward decls */ \ static ssize_t _write_callback_##func(struct file *filp, \ const char __user *buf, \ @@ -77,20 +76,30 @@ static char kftf_input_buf[128]; const char __user *buf, \ size_t len, loff_t *off) \ { \ - if (len >= sizeof(kftf_input_buf)) \ + pr_info("invoke %s\n", __FUNCTION__); \ + if (len >= sizeof(input_buf_##func)) { \ + mutex_unlock(&input_mutex_##func); \ return -EINVAL; \ - if (simple_write_to_buffer((void *)kftf_input_buf, \ - sizeof(kftf_input_buf) - 1, off, \ - buf, len) < 0) \ + } \ + if (simple_write_to_buffer((void *)input_buf_##func, \ + sizeof(input_buf_##func) - 1, off, \ + buf, len) < 0) { \ + pr_info("unable to read from buffer!\n"); \ + mutex_unlock(&input_mutex_##func); \ return -EFAULT; \ + } \ if (len != sizeof(func_arg_type)) { \ - pr_warn("incorrect data size\n"); \ + pr_info("incorrect data size\n"); \ + mutex_unlock(&input_mutex_##func); \ return -EINVAL; \ } \ - func_arg_type arg = *(func_arg_type *)kftf_input_buf; \ + /* XXX: no batching support, so just take the only elem */ \ + func_arg_type arg = input_buf_##func[0]; \ /* call the user's logic on the provided arg. */ \ /* NOTE: define some success/failure return types? */ \ + pr_info("invoking fuzz logic\n"); \ _fuzz_test_logic_##func(arg); \ + mutex_unlock(&input_mutex_##func); \ return len; \ } \ static void _fuzz_test_logic_##func(func_arg_type arg) From a4bb0025de9fb22b2291aec8c7a9bc69b203f432 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 23 Jun 2025 17:12:26 +0000 Subject: [PATCH 12/17] kftf: update kftf_fuzzable test --- lib/kftf/kftf_tests.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/kftf/kftf_tests.h b/lib/kftf/kftf_tests.h index b3895eebe5e7e3..7cc0a4676d0ec8 100644 --- a/lib/kftf/kftf_tests.h +++ b/lib/kftf/kftf_tests.h @@ -21,9 +21,8 @@ static void kftf_fuzzable(char first, char second, char third) pr_info("second was b"); if (third == 'c') { pr_info("third was c"); - /* do some weird access */ - char value = *(char *)&first + (first * 10 + 1); - pr_info("dumping ptr %c\b", value); + volatile char *ptr = (void *)0xBEEF; + pr_info("reading %p: 0x%x", ptr, *(uint *)ptr); } } } From 0bddc3ff15c75a18d4c811390cd4ec0d3e29c8f5 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 25 Jun 2025 07:54:15 +0000 Subject: [PATCH 13/17] kftf: add some comments to module main file and kftf header file --- include/linux/kftf.h | 188 ++++++++++++++++++++++++------------------- lib/kftf/kftf_main.c | 77 +++++++++++++++--- 2 files changed, 174 insertions(+), 91 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 80ede23d275d53..36c5f6775d788d 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -7,6 +7,21 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFTF)"); +/** + * struct kftf_test case defines a single fuzz test case. These should not + * be created manually. Instead the user should use the FUZZ_TEST macro defined + * below. + * @name: The name of the test case, generally the function being fuzzed. + * @arg_type_name: string representation of the type of argument being fuzzed, + * for example "struct func_arg_type" + * @write_input_cb: Callback invoked when a write's to the test case's debugfs + * input file. This is the entry point for fuzzing data. It is responsible + * for parsing any data written to the input file and invoking the fuzzing + * logic. It should return the number of bytes consumed from `buf` + * @read_metadata_cb: Callback invoked when a user reads from the test case's + * "metadata" debugfs file. It should simply return whatever is contained + * in the `arg_type_name` field. + */ struct kftf_test_case { const char *name; const char *arg_type_name; @@ -17,91 +32,102 @@ struct kftf_test_case { }; /** - * Expected usage: + * FUZZ_TEST - defines a fuzz test case for a function. + * @func: the function to be fuzzed. This is used to name the test case and + * create associated debufs entries. + * @func_arg_type: the input type of func. If func takes multiple arguments, + * then one should wrap that inside of a multi-fielded struct. See usage + * example below. + * + * + * This macro generates all of the necessary boilerplate for a KFTF test case, + * which is placed in a dedicated ".kftf" section so that the dedicated KFTF + * module can discover all defined tests at runtime. * - * ``` - * FUZZ_TEST(func, func_arg_type) { - * ret = func(arg.arg1, arg.arg2, ..., arg.argn); + * For each test, this macro generates + * - A buffer to receive input through the debugfs entry + * - A mutex to protect the input buffer + * - A `struct kftf_test_case` instance + * + * Example usagea: + * + * Assume some function `func(T1 param1, ... TN paramN)` + * // Define input type of the target function + * struct func_arg_type { + * T1 arg1; + * ... + * TN argn; + * }; + * + * // Define the test case + * FUZZ_TEST(func, struct func_arg_type) + * { + * // arg is provided by the macro, and is of type `struct func_arg_type` + * ret = func(arg.arg1, ..., arg.argn); * validate(ret); * } - * ``` - * - * The created structure will be registered by the kftf module, which creates - * a debugfs entry for it. The write callback is such that it accepts valid - * struct instances as input. */ -#define FUZZ_TEST(func, func_arg_type) \ - /* input buffer. Size 1 for now, but we may support batching */ \ - static func_arg_type input_buf_##func[2]; \ - /* guard the buffer as concurrent processes could race */ \ - DEFINE_MUTEX(input_mutex_##func); \ - /* forward decls */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off); \ - static ssize_t _read_metadata_callback_##func( \ - struct file *filp, char __user *buf, size_t len, loff_t *off); \ - static void _fuzz_test_logic_##func(func_arg_type arg); \ - /* test case struct initialization */ \ - const struct kftf_test_case __fuzz_test__##func \ - __attribute__((__section__(".kftf"), __used__)) = { \ - .name = #func, \ - .arg_type_name = #func_arg_type, \ - .write_input_cb = _write_callback_##func, \ - .read_metadata_cb = _read_metadata_callback_##func \ - }; \ - /* callback that simply returns the type name to the user */ \ - static ssize_t _read_metadata_callback_##func( \ - struct file *filp, char __user *buf, size_t len, loff_t *off) \ - { \ - int bytes_to_copy; \ - int message_len = strlen(__fuzz_test__##func.arg_type_name); \ - if (*off >= message_len) { \ - return -EINVAL; \ - } \ - bytes_to_copy = message_len - *off; \ - if (bytes_to_copy > len) { \ - bytes_to_copy = len; \ - } \ - if (copy_to_user(buf, \ - __fuzz_test__##func.arg_type_name + *off, \ - bytes_to_copy) != 0) { \ - return -EFAULT; \ - } \ - *off += bytes_to_copy; \ - return bytes_to_copy; \ - } \ - /* user-defined write callback */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off) \ - { \ - pr_info("invoke %s\n", __FUNCTION__); \ - if (len >= sizeof(input_buf_##func)) { \ - mutex_unlock(&input_mutex_##func); \ - return -EINVAL; \ - } \ - if (simple_write_to_buffer((void *)input_buf_##func, \ - sizeof(input_buf_##func) - 1, off, \ - buf, len) < 0) { \ - pr_info("unable to read from buffer!\n"); \ - mutex_unlock(&input_mutex_##func); \ - return -EFAULT; \ - } \ - if (len != sizeof(func_arg_type)) { \ - pr_info("incorrect data size\n"); \ - mutex_unlock(&input_mutex_##func); \ - return -EINVAL; \ - } \ - /* XXX: no batching support, so just take the only elem */ \ - func_arg_type arg = input_buf_##func[0]; \ - /* call the user's logic on the provided arg. */ \ - /* NOTE: define some success/failure return types? */ \ - pr_info("invoking fuzz logic\n"); \ - _fuzz_test_logic_##func(arg); \ - mutex_unlock(&input_mutex_##func); \ - return len; \ - } \ +#define FUZZ_TEST(func, func_arg_type) \ + /* The input buffer holds data written from userspace. Size 2 to \ + * support batching in the future, but currently only the first \ + * element is used. \ + */ \ + static func_arg_type input_buf_##func[2]; \ + /* guard the buffer as concurrent processes could race */ \ + DEFINE_MUTEX(input_mutex_##func); \ + /* forward decls */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off); \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off); \ + static void _fuzz_test_logic_##func(func_arg_type arg); \ + /* test case struct initialization */ \ + const struct kftf_test_case __fuzz_test__##func \ + __attribute__((__section__(".kftf"), __used__)) = { \ + .name = #func, \ + .arg_type_name = #func_arg_type, \ + .write_input_cb = _write_callback_##func, \ + .read_metadata_cb = _read_metadata_callback_##func \ + }; \ + /* callback that simply returns the type name to the user */ \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off) \ + { \ + const char *message = __fuzz_test__##func.arg_type_name; \ + int message_len = strlen(message); \ + return simple_read_from_buffer(buf, len, off, message, \ + message_len); \ + } \ + /* user-defined write callback */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ + { \ + if (len >= sizeof(input_buf_##func)) { \ + mutex_unlock(&input_mutex_##func); \ + return -EINVAL; \ + } \ + if (simple_write_to_buffer((void *)input_buf_##func, \ + sizeof(input_buf_##func) - 1, off, \ + buf, len) < 0) { \ + pr_warn("unable to read from buffer!\n"); \ + mutex_unlock(&input_mutex_##func); \ + return -EFAULT; \ + } \ + if (len != sizeof(func_arg_type)) { \ + pr_warn("incorrect data size\n"); \ + mutex_unlock(&input_mutex_##func); \ + return -EINVAL; \ + } \ + /* XXX: no batching support, so just take the only elem */ \ + func_arg_type arg = input_buf_##func[0]; \ + /* call the user's logic on the provided arg. */ \ + /* NOTE: define some success/failure return types? */ \ + _fuzz_test_logic_##func(arg); \ + mutex_unlock(&input_mutex_##func); \ + return len; \ + } \ static void _fuzz_test_logic_##func(func_arg_type arg) #endif /* KFTF_H */ diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index e83382e7be6ce0..9beb74f2b23d23 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -1,17 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Kernel Fuzz Testing Framework (KFTF) - Core Module + * + * This module is responsible for discovering and initializing all fuzz test + * cases defined using the FUZZ_TEST() macro. It creates a debugfs interface +* under /sys/kernel/debug/kftf/ for userspace to interact with each test. +*/ #include #include #include #include -#include "kftf_tests.h" - extern const struct kftf_test_case __kftf_start[]; extern const struct kftf_test_case __kftf_end[]; +/* + * KFTF_MAX_TEST_CASES - the hardcoded maximum number of test cases. + * + * To avoid kmalloc in this module, we use a statically allocated array to hold + * the state for each test case. This defines the upper limit on the size of + * that array. + */ #define KFTF_MAX_TEST_CASES 1024 /** - * defines a struct dentry with file-operations + * struct kftf_dentry - A container for a debugfs dentry and its fops. + * @dentry: Pointer to the created debugfs dentry. + * @fops: The file_operations struct associated with this dentry. + * + * This simplifies state management by keeping a file's dentry and its + * operations bundled together. */ struct kftf_dentry { struct dentry *dentry; @@ -19,7 +37,13 @@ struct kftf_dentry { }; /** - * Wraps teh state of the created + * struct kftf_debugfs_state - Per-test-case debugfs state. + * @test_dir: The top-level debugfs directory for a single test case, e.g., + * /sys/kernel/debug/kftf//. + * @input_dentry: The state for the "input" file, which is write-only. + * @metadata_dentry: The state for the "metadata" file, which is read-only. + * + * Wraps all debugfs components created for a single test case. */ struct kftf_debugfs_state { struct dentry *test_dir; @@ -27,6 +51,12 @@ struct kftf_debugfs_state { struct kftf_dentry metadata_dentry; }; +/** + * struct kftf_simple_fuzzer_state - Global state for the KFTF module. + * @kftf_dir: The root debugfs directory, /sys/kernel/debug/kftf/. + * @debugfs_state: A statically sized array holding the state for each + * registered test case. + */ struct kftf_simple_fuzzer_state { struct file_operations fops; struct dentry *kftf_dir; @@ -34,12 +64,36 @@ struct kftf_simple_fuzzer_state { debugfs_state[KFTF_MAX_TEST_CASES]; // FIXME: fine for WIP }; +/* Global static variable to hold all state for the module. */ static struct kftf_simple_fuzzer_state st; -/* XXX: Be careful of flags here. Should formally define what we want */ -const umode_t kftf_flags_w = 0666; +/* + * Default file permissions for the debugfs entries. + * 0222: World-writable for the 'input' file. + * 0444: World-readable for the 'metadata' file. + * + * XXX: should formally define what the permissions should be on these files + */ +const umode_t kftf_flags_w = 0222; const umode_t kftf_flags_r = 0444; +/** + * kftf_init - Initializes the debug filesystem for KFTF. + * + * This function is the entry point for the KFTF module, populating the debugfs + * that is used for IO interaction between the individual fuzzing drivers and + * a userspace fuzzing tool like syzkaller. + * + * Each registered test in the ".kftf" section gets its own subdirectory + * under "/sys/kernel/debug/kftf/" with two files: + * - input: write-only file to send input to the fuzz driver + * - metadata: used to read the type name that the fuzz driver expects + * + * Returns: + * 0 on success. + * -EINVAL if the number of tests exceeds KFTF_MAX_TEST_CASES + * -ENODEV or other error codes if debugfs creation fails. + */ static int __init kftf_init(void) { const struct kftf_test_case *test; @@ -47,14 +101,11 @@ static int __init kftf_init(void) int i = 0; size_t num_test_cases; - /* - * To avoid kmalloc entirely, we enforce a maximum number of fuzz tests - * that can be defined inside the kernel. - */ num_test_cases = __kftf_end - __kftf_start; if (num_test_cases > KFTF_MAX_TEST_CASES) return -EINVAL; + /* create the main "kftf" directory in `/sys/kernel/debug` */ st.kftf_dir = debugfs_create_dir("kftf", NULL); if (!st.kftf_dir) { pr_warn("kftf: could not create debugfs"); @@ -66,7 +117,9 @@ static int __init kftf_init(void) return PTR_ERR(st.kftf_dir); } + /* iterate over all discovered test cases and set up debugfs entries */ for (test = __kftf_start; test < __kftf_end; test++, i++) { + /* create a directory for the discovered test case */ st.debugfs_state[i].test_dir = debugfs_create_dir(test->name, st.kftf_dir); @@ -126,8 +179,12 @@ static int __init kftf_init(void) return ret; } +/** + * kftf_exit - Cleans up the module. + */ static void __exit kftf_exit(void) { + pr_info("kftf: shutting down\n"); if (!st.kftf_dir) return; From 597146743b10089cf0bd3875d9ab28e08fcb7199 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 25 Jun 2025 14:13:43 +0000 Subject: [PATCH 14/17] kftf: replace static test cases buffer with kmalloc'd one. --- lib/kftf/kftf_main.c | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/kftf/kftf_main.c b/lib/kftf/kftf_main.c index 9beb74f2b23d23..b151164d327802 100644 --- a/lib/kftf/kftf_main.c +++ b/lib/kftf/kftf_main.c @@ -14,15 +14,6 @@ extern const struct kftf_test_case __kftf_start[]; extern const struct kftf_test_case __kftf_end[]; -/* - * KFTF_MAX_TEST_CASES - the hardcoded maximum number of test cases. - * - * To avoid kmalloc in this module, we use a statically allocated array to hold - * the state for each test case. This defines the upper limit on the size of - * that array. - */ -#define KFTF_MAX_TEST_CASES 1024 - /** * struct kftf_dentry - A container for a debugfs dentry and its fops. * @dentry: Pointer to the created debugfs dentry. @@ -60,8 +51,7 @@ struct kftf_debugfs_state { struct kftf_simple_fuzzer_state { struct file_operations fops; struct dentry *kftf_dir; - struct kftf_debugfs_state - debugfs_state[KFTF_MAX_TEST_CASES]; // FIXME: fine for WIP + struct kftf_debugfs_state *debugfs_state; }; /* Global static variable to hold all state for the module. */ @@ -102,8 +92,11 @@ static int __init kftf_init(void) size_t num_test_cases; num_test_cases = __kftf_end - __kftf_start; - if (num_test_cases > KFTF_MAX_TEST_CASES) - return -EINVAL; + + st.debugfs_state = kmalloc( + num_test_cases * sizeof(struct kftf_debugfs_state), GFP_KERNEL); + if (!st.debugfs_state) + return -ENOMEM; /* create the main "kftf" directory in `/sys/kernel/debug` */ st.kftf_dir = debugfs_create_dir("kftf", NULL); From d799f5c2d00cb1e21ad7c66624cfaef7f164af4f Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 1 Jul 2025 06:35:00 +0000 Subject: [PATCH 15/17] kftf: replace input buffers for callback with dynamically allocated pointer to function argument type. This should remove the need for a mutex guarding a shared buffer entirely, and ultimately will likely perform better. --- include/linux/kftf.h | 119 +++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 36c5f6775d788d..918352115cc3f2 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -67,67 +67,64 @@ struct kftf_test_case { * validate(ret); * } */ -#define FUZZ_TEST(func, func_arg_type) \ - /* The input buffer holds data written from userspace. Size 2 to \ - * support batching in the future, but currently only the first \ - * element is used. \ - */ \ - static func_arg_type input_buf_##func[2]; \ - /* guard the buffer as concurrent processes could race */ \ - DEFINE_MUTEX(input_mutex_##func); \ - /* forward decls */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off); \ - static ssize_t _read_metadata_callback_##func( \ - struct file *filp, char __user *buf, size_t len, loff_t *off); \ - static void _fuzz_test_logic_##func(func_arg_type arg); \ - /* test case struct initialization */ \ - const struct kftf_test_case __fuzz_test__##func \ - __attribute__((__section__(".kftf"), __used__)) = { \ - .name = #func, \ - .arg_type_name = #func_arg_type, \ - .write_input_cb = _write_callback_##func, \ - .read_metadata_cb = _read_metadata_callback_##func \ - }; \ - /* callback that simply returns the type name to the user */ \ - static ssize_t _read_metadata_callback_##func( \ - struct file *filp, char __user *buf, size_t len, loff_t *off) \ - { \ - const char *message = __fuzz_test__##func.arg_type_name; \ - int message_len = strlen(message); \ - return simple_read_from_buffer(buf, len, off, message, \ - message_len); \ - } \ - /* user-defined write callback */ \ - static ssize_t _write_callback_##func(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off) \ - { \ - if (len >= sizeof(input_buf_##func)) { \ - mutex_unlock(&input_mutex_##func); \ - return -EINVAL; \ - } \ - if (simple_write_to_buffer((void *)input_buf_##func, \ - sizeof(input_buf_##func) - 1, off, \ - buf, len) < 0) { \ - pr_warn("unable to read from buffer!\n"); \ - mutex_unlock(&input_mutex_##func); \ - return -EFAULT; \ - } \ - if (len != sizeof(func_arg_type)) { \ - pr_warn("incorrect data size\n"); \ - mutex_unlock(&input_mutex_##func); \ - return -EINVAL; \ - } \ - /* XXX: no batching support, so just take the only elem */ \ - func_arg_type arg = input_buf_##func[0]; \ - /* call the user's logic on the provided arg. */ \ - /* NOTE: define some success/failure return types? */ \ - _fuzz_test_logic_##func(arg); \ - mutex_unlock(&input_mutex_##func); \ - return len; \ - } \ +#define FUZZ_TEST(func, func_arg_type) \ + /* forward decls */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off); \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off); \ + static void _fuzz_test_logic_##func(func_arg_type arg); \ + /* test case struct initialization */ \ + const struct kftf_test_case __fuzz_test__##func \ + __attribute__((__section__(".kftf"), __used__)) = { \ + .name = #func, \ + .arg_type_name = #func_arg_type, \ + .write_input_cb = _write_callback_##func, \ + .read_metadata_cb = _read_metadata_callback_##func \ + }; \ + /* callback that simply returns the type name to the user */ \ + static ssize_t _read_metadata_callback_##func( \ + struct file *filp, char __user *buf, size_t len, loff_t *off) \ + { \ + const char *message = __fuzz_test__##func.arg_type_name; \ + int message_len = strlen(message); \ + return simple_read_from_buffer(buf, len, off, message, \ + message_len); \ + } \ + /* user-defined write callback */ \ + static ssize_t _write_callback_##func(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ + { \ + func_arg_type *input_buf = \ + kmalloc(sizeof(func_arg_type), GFP_KERNEL); \ + if (!input_buf) \ + return -ENOMEM; \ + if (len >= sizeof(*input_buf)) { \ + kfree(input_buf); \ + return -EINVAL; \ + } \ + if (simple_write_to_buffer((void *)input_buf, \ + sizeof(*input_buf) - 1, off, buf, \ + len) < 0) { \ + pr_warn("unable to read from buffer!\n"); \ + kfree(input_buf); \ + return -EFAULT; \ + } \ + if (len != sizeof(func_arg_type)) { \ + pr_warn("incorrect data size\n"); \ + kfree(input_buf); \ + return -EINVAL; \ + } \ + /* XXX: no batching support yet */ \ + func_arg_type arg = *input_buf; \ + /* call the user's logic on the provided arg. */ \ + /* NOTE: define some success/failure return types? */ \ + _fuzz_test_logic_##func(arg); \ + kfree(input_buf); \ + return len; \ + } \ static void _fuzz_test_logic_##func(func_arg_type arg) #endif /* KFTF_H */ From aae72ff0d0fae79b86f32d4b6bbbb85347ad9f4a Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 1 Jul 2025 09:27:29 +0000 Subject: [PATCH 16/17] kftf: replace heap allocated input buffer with function-local stack variable, and extract common input reading functionality into a separate function. --- include/linux/kftf.h | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 918352115cc3f2..219baaee3b4ab6 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -31,6 +31,20 @@ struct kftf_test_case { loff_t *); }; +static int write_input_cb_common(struct file *filp, const char __user *buf, + size_t len, loff_t *off, void *arg, + size_t arg_size) +{ + if (len != arg_size) { + return -EINVAL; + } + if (simple_write_to_buffer(arg, sizeof(arg_size) - 1, off, buf, len) < + 0) { + return -EFAULT; + } + return 0; +} + /** * FUZZ_TEST - defines a fuzz test case for a function. * @func: the function to be fuzzed. This is used to name the test case and @@ -97,32 +111,17 @@ struct kftf_test_case { const char __user *buf, \ size_t len, loff_t *off) \ { \ - func_arg_type *input_buf = \ - kmalloc(sizeof(func_arg_type), GFP_KERNEL); \ - if (!input_buf) \ - return -ENOMEM; \ - if (len >= sizeof(*input_buf)) { \ - kfree(input_buf); \ - return -EINVAL; \ - } \ - if (simple_write_to_buffer((void *)input_buf, \ - sizeof(*input_buf) - 1, off, buf, \ - len) < 0) { \ - pr_warn("unable to read from buffer!\n"); \ - kfree(input_buf); \ - return -EFAULT; \ - } \ - if (len != sizeof(func_arg_type)) { \ - pr_warn("incorrect data size\n"); \ - kfree(input_buf); \ - return -EINVAL; \ + int err; \ + func_arg_type arg; \ + err = write_input_cb_common(filp, buf, len, off, &arg, \ + sizeof(arg)); \ + if (err != 0) { \ + return err; \ } \ - /* XXX: no batching support yet */ \ - func_arg_type arg = *input_buf; \ /* call the user's logic on the provided arg. */ \ /* NOTE: define some success/failure return types? */ \ + pr_info("invoking fuzz logic for %s\n", #func); \ _fuzz_test_logic_##func(arg); \ - kfree(input_buf); \ return len; \ } \ static void _fuzz_test_logic_##func(func_arg_type arg) From 72e3dec7208c9032ffb451799f6e08c3067420ce Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 1 Jul 2025 16:17:32 +0000 Subject: [PATCH 17/17] kftf: fix bug in input reading logic. --- include/linux/kftf.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/linux/kftf.h b/include/linux/kftf.h index 219baaee3b4ab6..ecfb3cb1c3695b 100644 --- a/include/linux/kftf.h +++ b/include/linux/kftf.h @@ -38,8 +38,7 @@ static int write_input_cb_common(struct file *filp, const char __user *buf, if (len != arg_size) { return -EINVAL; } - if (simple_write_to_buffer(arg, sizeof(arg_size) - 1, off, buf, len) < - 0) { + if (simple_write_to_buffer((void *)arg, arg_size, off, buf, len) < 0) { return -EFAULT; } return 0;