From fd61eeb89e6ec176a1ea8271e9288b56339b4b84 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 1 Aug 2025 13:46:37 +0000 Subject: [PATCH 01/41] linux/kfuzztest: update serialization format for KFuzzTest inputs --- include/linux/kfuzztest.h | 190 ++++++++++++++++++++++++-------------- 1 file changed, 123 insertions(+), 67 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index cae69cd9eb9c5c..d7214280fa89f5 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -7,7 +7,8 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); -static void *kfuzztest_parse_input(void *input, size_t input_size); +static void *kfuzztest_parse_input(void *input, size_t input_size, + size_t *num_regions); struct kfuzztest_target { const char *name; @@ -90,6 +91,8 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, size_t len, loff_t *off) \ { \ int err; \ + size_t i; \ + void *region, *regions; \ void *buffer = kmalloc(len, GFP_KERNEL); \ if (!buffer || IS_ERR(buffer)) \ return PTR_ERR(buffer); \ @@ -98,16 +101,27 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, kfree(buffer); \ return err; \ } \ - void *payload = kfuzztest_parse_input(buffer, len); \ - if (!payload) { \ + size_t num_regions; \ + regions = kfuzztest_parse_input(buffer, len, &num_regions); \ + if (!regions) { \ kfree(buffer); \ return -1; \ + } else if (IS_ERR(regions)) { \ + kfree(buffer); \ + return PTR_ERR(regions); \ } \ - test_arg_type *arg = payload; \ + pr_info("KFuzzTest: regions = 0x%px\n", regions); \ + /* The input argument is the first region. */ \ + test_arg_type *arg = ((void **)regions)[0]; \ /* Call the user's logic on the provided written input. */ \ _fuzz_test_logic_##test_name(arg); \ kfree(buffer); \ - kfree(payload); \ + for (i = 0; i < num_regions; i++) { \ + region = ((void **)regions)[i]; \ + if (region) \ + kfree(region); \ + } \ + kfree(regions); \ return len; \ } \ static void _fuzz_test_logic_##test_name(test_arg_type *arg) @@ -268,19 +282,20 @@ struct kfuzztest_annotation { __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) struct reloc_entry { - uintptr_t pointer; /* Offset from the beginning of the payload. */ - uintptr_t value; /* Offset between the pointer and the pointed-to address */ + uint64_t region_id; /* Region that pointer belongs to. */ + uint64_t region_offset; /* Offset from the beginning of the region. */ + uint64_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ + uint64_t padding; }; /* * How many integers of padding in the relocation table between the header * information and the relocation entries */ -#define RELOC_TABLE_PADDING 2 +#define RELOC_TABLE_PADDING 7 struct reloc_table { int num_entries; - uint32_t max_alignment; int padding[RELOC_TABLE_PADDING]; struct reloc_entry entries[]; }; @@ -288,6 +303,28 @@ static_assert(offsetof(struct reloc_table, entries) % sizeof(struct reloc_entry) == 0); +struct reloc_region { + uint64_t id; + uint64_t start; /* Offset from the start of the payload */ + uint64_t size; + uint64_t alignment; +}; + +/** + * How many `uint64_t`s of padding are required. + */ +#define RELOC_REGION_PADDING 3 + +struct reloc_region_array { + uint64_t num_regions; + uint64_t padding[RELOC_REGION_PADDING]; + struct reloc_region regions[]; +}; + +static_assert(offsetof(struct reloc_region_array, regions) % + sizeof(struct reloc_region) == + 0); + /** * The relocation table format encodes pointer values as a relative offset from * the location of the pointer. A relative offset of zero could indicate that @@ -297,94 +334,113 @@ static_assert(offsetof(struct reloc_table, entries) % */ static const uintptr_t nullPtr = (uintptr_t)-1; -__attribute__((unused)) static void *kfuzztest_parse_input(void *input, - size_t input_size) +__attribute__((unused)) static void * +kfuzztest_parse_input(void *input, size_t input_size, size_t *num_regions) { size_t i; - void *payload_start, *out; + void *payload_start; uintptr_t *ptr_location; - size_t payload_len, alloc_size, entries_size, header_size; + size_t payload_len, reloc_entries_size, reloc_table_size, + reloc_regions_size, region_array_size; struct reloc_table *rt; struct reloc_entry re; - if (input_size > KMALLOC_MAX_SIZE) - return NULL; - - if (input_size < sizeof(struct reloc_table)) { - pr_warn("got misformed input in %s\n", __FUNCTION__); + struct reloc_region_array *region_array; + struct reloc_region reg; + void **allocated_regions = NULL; + + pr_info("KFuzzTest: input [ 0x%px, 0x%px ]", input, + (char *)input + input_size); + if (input_size < + sizeof(struct reloc_table) + sizeof(struct reloc_region_array)) { + pr_warn("KFuzzTest: input was not well-formed"); return NULL; } rt = input; if (check_mul_overflow(rt->num_entries, sizeof(struct reloc_entry), - &entries_size)) + &reloc_entries_size)) return NULL; - header_size = offsetof(struct reloc_table, entries) + entries_size; - if (header_size > input_size) + reloc_table_size = + offsetof(struct reloc_table, entries) + reloc_entries_size; + pr_info("reloc_table_size: 0x%lx\n", reloc_table_size); + if (reloc_table_size > input_size) return NULL; + pr_info("num reloc entries: %d\n", rt->num_entries); - payload_start = (char *)input + header_size; - if (payload_start >= input + input_size) + region_array = + (struct reloc_region_array *)((char *)input + reloc_table_size); + if (check_mul_overflow(region_array->num_regions, + sizeof(struct reloc_region), + &reloc_regions_size)) return NULL; - if (!is_power_of_2(rt->max_alignment) || rt->max_alignment > PAGE_SIZE) + region_array_size = offsetof(struct reloc_region_array, regions) + + reloc_regions_size; + pr_info("region_array_size: 0x%lx", region_array_size); + pr_info("num regions = %llu", region_array->num_regions); + if (reloc_table_size + region_array_size > input_size) + return NULL; + + allocated_regions = + kzalloc(region_array->num_regions * sizeof(void *), GFP_KERNEL); + if (!allocated_regions) + return NULL; + + payload_start = (char *)region_array + region_array_size; + if (payload_start >= input + input_size) return NULL; payload_len = input_size - (payload_start - input); - /* - * Check input for out-of-bounds pointers before before allocating - * aligned output buffer. - */ - for (i = 0; i < rt->num_entries; i++) { - re = rt->entries[i]; - if (re.pointer > payload_len || - re.pointer + sizeof(uintptr_t) > payload_len) - return NULL; - - if (re.value == nullPtr) - continue; - - /* We don't know the size of the data that is being pointed and - * therefor cannot make any stricter assertions. For example, - * if we enforce: - * re.pointer + re.value + sizeof(uintptr_t) < payload_len - * then we cannot write values with bytesize < 8. - */ - if (re.pointer + re.value >= payload_len) - return NULL; - } + pr_info("KFuzzTest: allocating regions"); + /* Allocate regions, and copy data in. */ + for (i = 0; i < region_array->num_regions; i++) { + reg = region_array->regions[i]; - /* - * To guarantee correct alignment of structures within the payload, we - * allocate a new buffer that is aligned to the next power of two - * greater than either the size of the payload + 1 or the maximum - * alignment of the nested structures. We add one to the payload length - * and call kzalloc to ensure that the payload is padded by trailing - * zeros to prevent false-positives on non-null terminated strings. - */ - alloc_size = - MAX(roundup_pow_of_two(payload_len + 1), rt->max_alignment); - out = kzalloc(alloc_size, GFP_KERNEL); - if (!out) - return NULL; + /* kzalloc guarantees 8-byte alignment, which is enough. */ + allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); + if (!allocated_regions[i]) + goto fail; + + pr_info("copying from %px to %px with size 0x%llx", + (char *)payload_start + reg.start, allocated_regions[i], + reg.size); - memcpy(out, payload_start, payload_len); + memcpy(allocated_regions[i], (char *)payload_start + reg.start, + reg.size); + + pr_info("KFuzzTest: allocated region_%llu of size %llu\n", + reg.id, reg.size); + } - /* - * Iterate through entries in the relocation table and patch the - * pointers. - */ + /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; - ptr_location = (uintptr_t *)(out + re.pointer); + ptr_location = + (uintptr_t *)((char *)allocated_regions[re.region_id] + + re.region_offset); if (re.value == nullPtr) { *ptr_location = (uintptr_t)NULL; } else { - *ptr_location = (uintptr_t)ptr_location + re.value; + *ptr_location = (uintptr_t)allocated_regions[re.value]; + pr_info("KFuzzTest: pointer at offset %llu in region %llu pointer to region %llu (0x%px)", + re.region_offset, re.region_id, re.value, + (void *)*ptr_location); } } - return out; + if (num_regions) + *num_regions = region_array->num_regions; + return allocated_regions; + +fail: + if (!allocated_regions) + return NULL; + for (i = 0; i < region_array->num_regions; i++) { + if (allocated_regions[i]) + kfree(allocated_regions[i]); + } + return NULL; } #endif /* KFUZZTEST_H */ From c14dbe8c4e3e69cf362c9f924866f973d4f4b4da Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 4 Aug 2025 15:36:49 +0000 Subject: [PATCH 02/41] kfuzztest: reduce sizes of fields to 32-bit to reduce wastage --- include/linux/kfuzztest.h | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index d7214280fa89f5..df53ec61924fb5 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -282,21 +282,21 @@ struct kfuzztest_annotation { __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) struct reloc_entry { - uint64_t region_id; /* Region that pointer belongs to. */ - uint64_t region_offset; /* Offset from the beginning of the region. */ - uint64_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ - uint64_t padding; + uint32_t region_id; /* Region that pointer belongs to. */ + uint32_t region_offset; /* Offset from the beginning of the region. */ + uint32_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ + uint32_t padding; }; /* * How many integers of padding in the relocation table between the header * information and the relocation entries */ -#define RELOC_TABLE_PADDING 7 +#define RELOC_TABLE_PADDING 3 struct reloc_table { - int num_entries; - int padding[RELOC_TABLE_PADDING]; + uint32_t num_entries; + uint32_t padding[RELOC_TABLE_PADDING]; struct reloc_entry entries[]; }; static_assert(offsetof(struct reloc_table, entries) % @@ -304,20 +304,23 @@ static_assert(offsetof(struct reloc_table, entries) % 0); struct reloc_region { - uint64_t id; - uint64_t start; /* Offset from the start of the payload */ - uint64_t size; - uint64_t alignment; + uint32_t id; + uint32_t start; /* Offset from the start of the payload */ + uint32_t size; + uint32_t alignment; }; +enum reloc_mode : uint32_t { DISTINCT = 0, POISONED }; + /** * How many `uint64_t`s of padding are required. */ -#define RELOC_REGION_PADDING 3 +#define RELOC_REGION_PADDING 2 struct reloc_region_array { - uint64_t num_regions; - uint64_t padding[RELOC_REGION_PADDING]; + uint32_t num_regions; + enum reloc_mode mode; + uint32_t padding[RELOC_REGION_PADDING]; struct reloc_region regions[]; }; From e763e701b17a81af44cbf3b3c1c427b17cfe0e87 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 4 Aug 2025 18:01:57 +0000 Subject: [PATCH 03/41] kfuzztest: refactor code, introduce distinct and poisoned modes. WIP. Lots of validation to introduce, and more refactoring so that data doesn't leak. --- include/linux/kfuzztest.h | 358 +++++++++++++++++++++++++------------- 1 file changed, 234 insertions(+), 124 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index df53ec61924fb5..030b24835733e0 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -2,13 +2,77 @@ #define KFUZZTEST_H #include +#include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); -static void *kfuzztest_parse_input(void *input, size_t input_size, - size_t *num_regions); +struct reloc_region { + uint32_t start; /* Offset from the start of the payload */ + uint32_t size; + uint32_t alignment; + uint32_t padding; +}; + +enum reloc_mode : uint32_t { DISTINCT = 0, POISONED }; + +struct reloc_region_array { + uint32_t num_regions; + enum reloc_mode mode; + uint32_t padding[2]; + struct reloc_region regions[]; +}; + +struct reloc_entry { + uint32_t region_id; /* Region that pointer belongs to. */ + uint32_t region_offset; /* Offset from the beginning of the region. */ + uint32_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ + uint32_t padding; +}; + +struct reloc_table { + uint32_t num_entries; + uint32_t padding[3]; + struct reloc_entry entries[]; +}; +static_assert(offsetof(struct reloc_table, entries) % + sizeof(struct reloc_entry) == + 0); + +static_assert(offsetof(struct reloc_region_array, regions) % + sizeof(struct reloc_region) == + 0); + +typedef void *reloc_handle_t; + +__attribute__((unused)) static int __kfuzztest_parse_input( + void *input, size_t input_size, struct reloc_region_array **ret_regions, + struct reloc_table **ret_reloc_table, void **ret_payload); + +__attribute__((unused)) static reloc_handle_t +__kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, + void *payload, void **data_ret); + +static reloc_handle_t +__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret); + +static reloc_handle_t +__kfuzztest_relocate_distinct(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret); + +__attribute__((unused)) static void +__kfuzztest_release_relocated(struct reloc_region_array *regions, + reloc_handle_t handle); + +static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle); + +static void +__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, + reloc_handle_t handle); struct kfuzztest_target { const char *name; @@ -91,37 +155,37 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, size_t len, loff_t *off) \ { \ int err; \ - size_t i; \ - void *region, *regions; \ + struct reloc_region_array *regions; \ + struct reloc_table *rt; \ + void *payload, *relocated; \ + test_arg_type *arg; \ + \ void *buffer = kmalloc(len, GFP_KERNEL); \ - if (!buffer || IS_ERR(buffer)) \ + if (!buffer) \ + return -ENOMEM; \ + else if (IS_ERR(buffer)) \ return PTR_ERR(buffer); \ err = write_input_cb_common(filp, buf, len, off, buffer, len); \ if (err != 0) { \ kfree(buffer); \ return err; \ } \ - size_t num_regions; \ - regions = kfuzztest_parse_input(buffer, len, &num_regions); \ - if (!regions) { \ + err = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ + &payload); \ + if (err) { \ kfree(buffer); \ - return -1; \ - } else if (IS_ERR(regions)) { \ + return err; \ + } \ + relocated = __kfuzztest_relocate(regions, rt, payload, \ + (void *)&arg); \ + if (!relocated) { \ kfree(buffer); \ - return PTR_ERR(regions); \ + return -EINVAL; \ } \ - pr_info("KFuzzTest: regions = 0x%px\n", regions); \ - /* The input argument is the first region. */ \ - test_arg_type *arg = ((void **)regions)[0]; \ /* Call the user's logic on the provided written input. */ \ _fuzz_test_logic_##test_name(arg); \ - kfree(buffer); \ - for (i = 0; i < num_regions; i++) { \ - region = ((void **)regions)[i]; \ - if (region) \ - kfree(region); \ - } \ - kfree(regions); \ + /* XXX: buffer leaks in distinct mode. */ \ + __kfuzztest_release_relocated(regions, relocated); \ return len; \ } \ static void _fuzz_test_logic_##test_name(test_arg_type *arg) @@ -281,142 +345,155 @@ struct kfuzztest_annotation { #define KFUZZTEST_ANNOTATE_LEN(arg_type, field, linked_field) \ __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) -struct reloc_entry { - uint32_t region_id; /* Region that pointer belongs to. */ - uint32_t region_offset; /* Offset from the beginning of the region. */ - uint32_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ - uint32_t padding; -}; - -/* - * How many integers of padding in the relocation table between the header - * information and the relocation entries +/** + * The relocation table format encodes pointer values as a relative offset from + * the location of the pointer. A relative offset of zero could indicate that + * the pointer points to its own address, which is valid. We encode a null + * pointer as 0xFF...FF as adding this value to any address would result in an + * overflow anyways, and is therefore invalid in any other circumstance. */ -#define RELOC_TABLE_PADDING 3 +static const uintptr_t nullPtr = (uint32_t)-1; -struct reloc_table { - uint32_t num_entries; - uint32_t padding[RELOC_TABLE_PADDING]; - struct reloc_entry entries[]; -}; -static_assert(offsetof(struct reloc_table, entries) % - sizeof(struct reloc_entry) == - 0); +/* Performs some input validation before relocating data. */ +__attribute__((unused)) static int __kfuzztest_parse_input( + void *input, size_t input_size, struct reloc_region_array **ret_regions, + struct reloc_table **ret_reloc_table, void **ret_payload) +{ + void *input_end, *payload_start; + size_t reloc_entries_size, regions_size; + struct reloc_table *rt; + struct reloc_region_array *regions; -struct reloc_region { - uint32_t id; - uint32_t start; /* Offset from the start of the payload */ - uint32_t size; - uint32_t alignment; -}; + if (input_size < + sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) + return -EINVAL; -enum reloc_mode : uint32_t { DISTINCT = 0, POISONED }; + input_end = (char *)input + input_size; -/** - * How many `uint64_t`s of padding are required. - */ -#define RELOC_REGION_PADDING 2 + regions = input; + regions_size = sizeof(struct reloc_region_array) + + regions->num_regions * sizeof(struct reloc_region); -struct reloc_region_array { - uint32_t num_regions; - enum reloc_mode mode; - uint32_t padding[RELOC_REGION_PADDING]; - struct reloc_region regions[]; -}; + rt = (struct reloc_table *)((char *)regions + regions_size); + if ((char *)rt > (char *)input_end) + return -EINVAL; -static_assert(offsetof(struct reloc_region_array, regions) % - sizeof(struct reloc_region) == - 0); + reloc_entries_size = sizeof(struct reloc_table) + + rt->num_entries * sizeof(struct reloc_entry); + if ((char *)rt + reloc_entries_size > (char *)input_end) + return -EINVAL; + + payload_start = (char *)(rt->entries + rt->num_entries); + if ((char *)payload_start > (char *)input_end) + return -EINVAL; + + if (ret_regions) + *ret_regions = regions; + if (ret_reloc_table) + *ret_reloc_table = rt; + if (ret_payload) + *ret_payload = payload_start; + return 0; +} /** - * The relocation table format encodes pointer values as a relative offset from - * the location of the pointer. A relative offset of zero could indicate that - * the pointer points to its own address, which is valid. We encode a null - * pointer as 0xFF...FF as adding this value to any address would result in an - * overflow anyways, and is therefore invalid in any other circumstance. + * Relocates payload as per `regions` and `rt`. Assumes that the input is + * already sanitized. */ -static const uintptr_t nullPtr = (uintptr_t)-1; +__attribute__((unused)) static reloc_handle_t +__kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, + void *payload, void **data_ret) +{ + switch (regions->mode) { + case DISTINCT: + return __kfuzztest_relocate_distinct(regions, rt, payload, + data_ret); + case POISONED: + return __kfuzztest_relocate_poisoned(regions, rt, payload, + data_ret); + default: + return ERR_PTR(-EINVAL); + } +} -__attribute__((unused)) static void * -kfuzztest_parse_input(void *input, size_t input_size, size_t *num_regions) +static reloc_handle_t +__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret) { size_t i; - void *payload_start; + struct reloc_region reg, src, dst; uintptr_t *ptr_location; - size_t payload_len, reloc_entries_size, reloc_table_size, - reloc_regions_size, region_array_size; - struct reloc_table *rt; struct reloc_entry re; - struct reloc_region_array *region_array; - struct reloc_region reg; - void **allocated_regions = NULL; + void *ptr; - pr_info("KFuzzTest: input [ 0x%px, 0x%px ]", input, - (char *)input + input_size); - if (input_size < - sizeof(struct reloc_table) + sizeof(struct reloc_region_array)) { - pr_warn("KFuzzTest: input was not well-formed"); - return NULL; + /* Poison the padding between regions */ + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + ptr = payload + reg.start + reg.size; + // TODO: poison this range. Figure out import for kasan.h. } - rt = input; - if (check_mul_overflow(rt->num_entries, sizeof(struct reloc_entry), - &reloc_entries_size)) - return NULL; - reloc_table_size = - offsetof(struct reloc_table, entries) + reloc_entries_size; - pr_info("reloc_table_size: 0x%lx\n", reloc_table_size); - if (reloc_table_size > input_size) - return NULL; - pr_info("num reloc entries: %d\n", rt->num_entries); + // TODO: failure when the region points out of bounds. - region_array = - (struct reloc_region_array *)((char *)input + reloc_table_size); - if (check_mul_overflow(region_array->num_regions, - sizeof(struct reloc_region), - &reloc_regions_size)) - return NULL; + /* Patch pointers */ + for (i = 0; i < rt->num_entries; i++) { + re = rt->entries[i]; + src = regions->regions[re.region_id]; + ptr_location = (uintptr_t *)(char *)payload + src.start + + re.region_offset; - region_array_size = offsetof(struct reloc_region_array, regions) + - reloc_regions_size; - pr_info("region_array_size: 0x%lx", region_array_size); - pr_info("num regions = %llu", region_array->num_regions); - if (reloc_table_size + region_array_size > input_size) - return NULL; + if (re.value == nullPtr) { + *ptr_location = (uintptr_t)NULL; + } else { + if (re.value >= regions->num_regions) + goto fail; + dst = regions->regions[re.value]; + *ptr_location = (uintptr_t)(char *)payload + dst.start; + } + } + + return regions; +fail: + __kfuzztest_release_relocated_poisoned(regions); + return NULL; +} + +static reloc_handle_t +__kfuzztest_relocate_distinct(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret) +{ + void **allocated_regions; + size_t i; + struct reloc_region reg; + struct reloc_entry re; + uintptr_t *ptr_location; allocated_regions = - kzalloc(region_array->num_regions * sizeof(void *), GFP_KERNEL); + kzalloc(regions->num_regions * sizeof(void *), GFP_KERNEL); if (!allocated_regions) return NULL; - payload_start = (char *)region_array + region_array_size; - if (payload_start >= input + input_size) - return NULL; - - payload_len = input_size - (payload_start - input); - - pr_info("KFuzzTest: allocating regions"); - /* Allocate regions, and copy data in. */ - for (i = 0; i < region_array->num_regions; i++) { - reg = region_array->regions[i]; + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; /* kzalloc guarantees 8-byte alignment, which is enough. */ allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); if (!allocated_regions[i]) goto fail; - pr_info("copying from %px to %px with size 0x%llx", - (char *)payload_start + reg.start, allocated_regions[i], + pr_info("copying from %px to %px with size 0x%x", + (char *)payload + reg.start, allocated_regions[i], reg.size); - memcpy(allocated_regions[i], (char *)payload_start + reg.start, + memcpy(allocated_regions[i], (char *)payload + reg.start, reg.size); - pr_info("KFuzzTest: allocated region_%llu of size %llu\n", - reg.id, reg.size); + pr_info("KFuzzTest: allocated region_%zu of size %u\n", i, + reg.size); } - /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; ptr_location = @@ -426,24 +503,57 @@ kfuzztest_parse_input(void *input, size_t input_size, size_t *num_regions) *ptr_location = (uintptr_t)NULL; } else { *ptr_location = (uintptr_t)allocated_regions[re.value]; - pr_info("KFuzzTest: pointer at offset %llu in region %llu pointer to region %llu (0x%px)", + pr_info("KFuzzTest: pointer at offset %u in region %u pointer to region %u (0x%px)", re.region_offset, re.region_id, re.value, (void *)*ptr_location); } } - if (num_regions) - *num_regions = region_array->num_regions; + if (data_ret) + *data_ret = allocated_regions[0]; return allocated_regions; fail: if (!allocated_regions) return NULL; - for (i = 0; i < region_array->num_regions; i++) { + for (i = 0; i < regions->num_regions; i++) { if (allocated_regions[i]) kfree(allocated_regions[i]); } return NULL; } +__attribute__((unused)) static void +__kfuzztest_release_relocated(struct reloc_region_array *regions, + reloc_handle_t handle) +{ + switch (regions->mode) { + case DISTINCT: + __kfuzztest_release_relocated_distinct(regions, handle); + break; + case POISONED: + __kfuzztest_release_relocated_poisoned(handle); + break; + default: + pr_warn("KFuzzTest: invalid release operation"); + } +} + +static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle) +{ + kfree(handle); +} + +static void +__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, + reloc_handle_t handle) +{ + size_t i; + void **allocations = (void **)handle; + for (i = 0; i < regions->num_regions; i++) + if (allocations[i]) + kfree(allocations[i]); + kfree((void *)handle); +} + #endif /* KFUZZTEST_H */ From 76c3b85f08a3572604a0d65a0ec1e77c3faa9e5e Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 06:41:19 +0000 Subject: [PATCH 04/41] kfuzztest: extract relocations logic into separate .c file. --- include/linux/kfuzztest.h | 187 +----------------------------------- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/relocations.c | 157 ++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 183 deletions(-) create mode 100644 lib/kfuzztest/relocations.c diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 030b24835733e0..97ca5471d57ee1 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -50,29 +50,12 @@ __attribute__((unused)) static int __kfuzztest_parse_input( void *input, size_t input_size, struct reloc_region_array **ret_regions, struct reloc_table **ret_reloc_table, void **ret_payload); -__attribute__((unused)) static reloc_handle_t -__kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, - void *payload, void **data_ret); +reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret); -static reloc_handle_t -__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret); - -static reloc_handle_t -__kfuzztest_relocate_distinct(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret); - -__attribute__((unused)) static void -__kfuzztest_release_relocated(struct reloc_region_array *regions, - reloc_handle_t handle); - -static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle); - -static void -__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, - reloc_handle_t handle); +void __kfuzztest_release_relocated(struct reloc_region_array *regions, + reloc_handle_t handle); struct kfuzztest_target { const char *name; @@ -396,164 +379,4 @@ __attribute__((unused)) static int __kfuzztest_parse_input( return 0; } -/** - * Relocates payload as per `regions` and `rt`. Assumes that the input is - * already sanitized. - */ -__attribute__((unused)) static reloc_handle_t -__kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, - void *payload, void **data_ret) -{ - switch (regions->mode) { - case DISTINCT: - return __kfuzztest_relocate_distinct(regions, rt, payload, - data_ret); - case POISONED: - return __kfuzztest_relocate_poisoned(regions, rt, payload, - data_ret); - default: - return ERR_PTR(-EINVAL); - } -} - -static reloc_handle_t -__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret) -{ - size_t i; - struct reloc_region reg, src, dst; - uintptr_t *ptr_location; - struct reloc_entry re; - void *ptr; - - /* Poison the padding between regions */ - for (i = 0; i < regions->num_regions; i++) { - reg = regions->regions[i]; - ptr = payload + reg.start + reg.size; - // TODO: poison this range. Figure out import for kasan.h. - } - - // TODO: failure when the region points out of bounds. - - /* Patch pointers */ - for (i = 0; i < rt->num_entries; i++) { - re = rt->entries[i]; - src = regions->regions[re.region_id]; - ptr_location = (uintptr_t *)(char *)payload + src.start + - re.region_offset; - - if (re.value == nullPtr) { - *ptr_location = (uintptr_t)NULL; - } else { - if (re.value >= regions->num_regions) - goto fail; - dst = regions->regions[re.value]; - *ptr_location = (uintptr_t)(char *)payload + dst.start; - } - } - - return regions; -fail: - __kfuzztest_release_relocated_poisoned(regions); - return NULL; -} - -static reloc_handle_t -__kfuzztest_relocate_distinct(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret) -{ - void **allocated_regions; - size_t i; - struct reloc_region reg; - struct reloc_entry re; - uintptr_t *ptr_location; - - allocated_regions = - kzalloc(regions->num_regions * sizeof(void *), GFP_KERNEL); - if (!allocated_regions) - return NULL; - - for (i = 0; i < regions->num_regions; i++) { - reg = regions->regions[i]; - - /* kzalloc guarantees 8-byte alignment, which is enough. */ - allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); - if (!allocated_regions[i]) - goto fail; - - pr_info("copying from %px to %px with size 0x%x", - (char *)payload + reg.start, allocated_regions[i], - reg.size); - - memcpy(allocated_regions[i], (char *)payload + reg.start, - reg.size); - - pr_info("KFuzzTest: allocated region_%zu of size %u\n", i, - reg.size); - } - - for (i = 0; i < rt->num_entries; i++) { - re = rt->entries[i]; - ptr_location = - (uintptr_t *)((char *)allocated_regions[re.region_id] + - re.region_offset); - if (re.value == nullPtr) { - *ptr_location = (uintptr_t)NULL; - } else { - *ptr_location = (uintptr_t)allocated_regions[re.value]; - pr_info("KFuzzTest: pointer at offset %u in region %u pointer to region %u (0x%px)", - re.region_offset, re.region_id, re.value, - (void *)*ptr_location); - } - } - - if (data_ret) - *data_ret = allocated_regions[0]; - return allocated_regions; - -fail: - if (!allocated_regions) - return NULL; - for (i = 0; i < regions->num_regions; i++) { - if (allocated_regions[i]) - kfree(allocated_regions[i]); - } - return NULL; -} - -__attribute__((unused)) static void -__kfuzztest_release_relocated(struct reloc_region_array *regions, - reloc_handle_t handle) -{ - switch (regions->mode) { - case DISTINCT: - __kfuzztest_release_relocated_distinct(regions, handle); - break; - case POISONED: - __kfuzztest_release_relocated_poisoned(handle); - break; - default: - pr_warn("KFuzzTest: invalid release operation"); - } -} - -static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle) -{ - kfree(handle); -} - -static void -__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, - reloc_handle_t handle) -{ - size_t i; - void **allocations = (void **)handle; - for (i = 0; i < regions->num_regions; i++) - if (allocations[i]) - kfree(allocations[i]); - kfree((void *)handle); -} - #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index ddaab873324c2a..462f229fdabdbd 100644 --- a/lib/kfuzztest/Makefile +++ b/lib/kfuzztest/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only obj-$(CONFIG_KFUZZTEST) += kfuzztest.o -kfuzztest-objs := kfuzztest_main.o +kfuzztest-objs := kfuzztest_main.o relocations.o diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c new file mode 100644 index 00000000000000..c2a2de1254b5e1 --- /dev/null +++ b/lib/kfuzztest/relocations.c @@ -0,0 +1,157 @@ +#include +#include + +static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle) +{ + kfree(handle); +} + +static void +__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, + reloc_handle_t handle) +{ + size_t i; + void **allocations = (void **)handle; + for (i = 0; i < regions->num_regions; i++) + if (allocations[i]) + kfree(allocations[i]); + kfree((void *)handle); +} + +void __kfuzztest_release_relocated(struct reloc_region_array *regions, + reloc_handle_t handle) +{ + switch (regions->mode) { + case DISTINCT: + __kfuzztest_release_relocated_distinct(regions, handle); + break; + case POISONED: + __kfuzztest_release_relocated_poisoned(handle); + break; + default: + pr_warn("KFuzzTest: invalid release operation"); + } +} + +static reloc_handle_t +__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret) +{ + size_t i; + struct reloc_region reg, src, dst; + uintptr_t *ptr_location; + struct reloc_entry re; + void *ptr; + + /* Poison the padding between regions */ + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + ptr = payload + reg.start + reg.size; + // TODO: poison this range. Figure out import for kasan.h. + } + + // TODO: failure when the region points out of bounds. + + /* Patch pointers */ + for (i = 0; i < rt->num_entries; i++) { + re = rt->entries[i]; + src = regions->regions[re.region_id]; + ptr_location = (uintptr_t *)(char *)payload + src.start + + re.region_offset; + + if (re.value == nullPtr) { + *ptr_location = (uintptr_t)NULL; + } else { + if (re.value >= regions->num_regions) + goto fail; + dst = regions->regions[re.value]; + *ptr_location = (uintptr_t)(char *)payload + dst.start; + } + } + + return regions; +fail: + __kfuzztest_release_relocated_poisoned(regions); + return NULL; +} + +static reloc_handle_t +__kfuzztest_relocate_distinct(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret) +{ + void **allocated_regions; + size_t i; + struct reloc_region reg; + struct reloc_entry re; + uintptr_t *ptr_location; + + allocated_regions = + kzalloc(regions->num_regions * sizeof(void *), GFP_KERNEL); + if (!allocated_regions) + return NULL; + + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + + /* kzalloc guarantees 8-byte alignment, which is enough. */ + allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); + if (!allocated_regions[i]) + goto fail; + + pr_info("copying from %px to %px with size 0x%x", + (char *)payload + reg.start, allocated_regions[i], + reg.size); + + memcpy(allocated_regions[i], (char *)payload + reg.start, + reg.size); + + pr_info("KFuzzTest: allocated region_%zu of size %u\n", i, + reg.size); + } + + for (i = 0; i < rt->num_entries; i++) { + re = rt->entries[i]; + ptr_location = + (uintptr_t *)((char *)allocated_regions[re.region_id] + + re.region_offset); + if (re.value == nullPtr) { + *ptr_location = (uintptr_t)NULL; + } else { + *ptr_location = (uintptr_t)allocated_regions[re.value]; + pr_info("KFuzzTest: pointer at offset %u in region %u pointer to region %u (0x%px)", + re.region_offset, re.region_id, re.value, + (void *)*ptr_location); + } + } + + if (data_ret) + *data_ret = allocated_regions[0]; + return allocated_regions; + +fail: + if (!allocated_regions) + return NULL; + for (i = 0; i < regions->num_regions; i++) { + if (allocated_regions[i]) + kfree(allocated_regions[i]); + } + return NULL; +} + +reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload, + void **data_ret) +{ + switch (regions->mode) { + case DISTINCT: + return __kfuzztest_relocate_distinct(regions, rt, payload, + data_ret); + case POISONED: + return __kfuzztest_relocate_poisoned(regions, rt, payload, + data_ret); + default: + return ERR_PTR(-EINVAL); + } +} From f0bd54aded2e85c66279b73bc929c62f1e62db00 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 08:52:01 +0000 Subject: [PATCH 05/41] kfuzztest: refactoring functions into separate .c files, modify kasan Export kasan_poison by moving it to include/linux. Move lots of things around into separate .c files to keep kfuzztest.h barebones. --- include/linux/kasan.h | 2 + include/linux/kfuzztest.h | 116 ++++++++++++++++-------------------- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/parse.c | 51 ++++++++++++++++ lib/kfuzztest/relocations.c | 91 +++++++++++++++------------- mm/kasan/kasan.h | 9 --- 6 files changed, 152 insertions(+), 119 deletions(-) create mode 100644 lib/kfuzztest/parse.c diff --git a/include/linux/kasan.h b/include/linux/kasan.h index 890011071f2b14..d6dba15bc57555 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -126,6 +126,8 @@ static __always_inline bool kasan_unpoison_pages(struct page *page, return false; } +void kasan_poison(const void *addr, size_t size, u8 value, bool init); + void __kasan_poison_slab(struct slab *slab); static __always_inline void kasan_poison_slab(struct slab *slab) { diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 97ca5471d57ee1..dbe60f9533a5d4 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -2,20 +2,19 @@ #define KFUZZTEST_H #include -#include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); struct reloc_region { - uint32_t start; /* Offset from the start of the payload */ + uint32_t start; /* Offset from the start of the payload. */ uint32_t size; uint32_t alignment; uint32_t padding; }; -enum reloc_mode : uint32_t { DISTINCT = 0, POISONED }; +enum reloc_mode { DISTINCT = 0, POISONED }; struct reloc_region_array { uint32_t num_regions; @@ -24,6 +23,10 @@ struct reloc_region_array { struct reloc_region regions[]; }; +static_assert(offsetof(struct reloc_region_array, regions) % + sizeof(struct reloc_region) == + 0); + struct reloc_entry { uint32_t region_id; /* Region that pointer belongs to. */ uint32_t region_offset; /* Offset from the beginning of the region. */ @@ -40,20 +43,42 @@ static_assert(offsetof(struct reloc_table, entries) % sizeof(struct reloc_entry) == 0); -static_assert(offsetof(struct reloc_region_array, regions) % - sizeof(struct reloc_region) == - 0); - +/** + * Internal handle used for releasing resources, returned to the caller of + * __kfuzztest_relocate. + */ typedef void *reloc_handle_t; -__attribute__((unused)) static int __kfuzztest_parse_input( - void *input, size_t input_size, struct reloc_region_array **ret_regions, - struct reloc_table **ret_reloc_table, void **ret_payload); +/** + * Parses a binary input of size input_size. Input should be a pointer to a + * heap-allocated buffer, and it's ownership is transferred to this function + * on call. + * @input: a heap-allocated buffer (ownership transferred). + * @input_size: the byte-length of input + * @ret_regions: return pointer to the relocation region array + * @ret_reloc_table: return pointer to the relocation table + * @ret_payload_start: return pointer to the start of payload the data + * @ret_payload_end: return pointer to the end of the payload data, i.e., the + * first address that is out of the bounds of the payload. + * + * @return 0 on success, or an error code. + */ +int __kfuzztest_parse_input(void *input, size_t input_size, + struct reloc_region_array **ret_regions, + struct reloc_table **ret_reloc_table, + void **ret_payload_start, void **ret_payload_end); +/** + * Relocates a parsed input into kernel memory. + */ reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret); + struct reloc_table *rt, void *payload_start, + void *payload_end, void **data_ret); +/** + * Release the relocated data, freeing the initial buffer that was copied from + * user space. + */ void __kfuzztest_release_relocated(struct reloc_region_array *regions, reloc_handle_t handle); @@ -140,7 +165,7 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, int err; \ struct reloc_region_array *regions; \ struct reloc_table *rt; \ - void *payload, *relocated; \ + void *payload_start, *payload_end, *relocated; \ test_arg_type *arg; \ \ void *buffer = kmalloc(len, GFP_KERNEL); \ @@ -154,20 +179,17 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, return err; \ } \ err = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ - &payload); \ - if (err) { \ - kfree(buffer); \ + &payload_start, &payload_end); \ + if (err) \ return err; \ - } \ - relocated = __kfuzztest_relocate(regions, rt, payload, \ - (void *)&arg); \ - if (!relocated) { \ - kfree(buffer); \ + /* Frees `buffer` on failure. */ \ + relocated = __kfuzztest_relocate(regions, rt, payload_start, \ + payload_end, (void *)&arg); \ + if (!relocated) \ return -EINVAL; \ - } \ /* Call the user's logic on the provided written input. */ \ _fuzz_test_logic_##test_name(arg); \ - /* XXX: buffer leaks in distinct mode. */ \ + /* Frees `buffer`. */ \ __kfuzztest_release_relocated(regions, relocated); \ return len; \ } \ @@ -332,51 +354,13 @@ struct kfuzztest_annotation { * The relocation table format encodes pointer values as a relative offset from * the location of the pointer. A relative offset of zero could indicate that * the pointer points to its own address, which is valid. We encode a null - * pointer as 0xFF...FF as adding this value to any address would result in an + * pointer as 0xFFFFFFFF as adding this value to any address would result in an * overflow anyways, and is therefore invalid in any other circumstance. */ -static const uintptr_t nullPtr = (uint32_t)-1; - -/* Performs some input validation before relocating data. */ -__attribute__((unused)) static int __kfuzztest_parse_input( - void *input, size_t input_size, struct reloc_region_array **ret_regions, - struct reloc_table **ret_reloc_table, void **ret_payload) -{ - void *input_end, *payload_start; - size_t reloc_entries_size, regions_size; - struct reloc_table *rt; - struct reloc_region_array *regions; - - if (input_size < - sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) - return -EINVAL; - - input_end = (char *)input + input_size; - - regions = input; - regions_size = sizeof(struct reloc_region_array) + - regions->num_regions * sizeof(struct reloc_region); +#define KFUZZTEST_REGIONID_NULL U32_MAX - rt = (struct reloc_table *)((char *)regions + regions_size); - if ((char *)rt > (char *)input_end) - return -EINVAL; - - reloc_entries_size = sizeof(struct reloc_table) + - rt->num_entries * sizeof(struct reloc_entry); - if ((char *)rt + reloc_entries_size > (char *)input_end) - return -EINVAL; - - payload_start = (char *)(rt->entries + rt->num_entries); - if ((char *)payload_start > (char *)input_end) - return -EINVAL; - - if (ret_regions) - *ret_regions = regions; - if (ret_reloc_table) - *ret_reloc_table = rt; - if (ret_payload) - *ret_payload = payload_start; - return 0; -} +/* Performs some input validation, and returns the rerloc region array, and + * reloc table. + */ #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index 462f229fdabdbd..60414722c0c073 100644 --- a/lib/kfuzztest/Makefile +++ b/lib/kfuzztest/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only obj-$(CONFIG_KFUZZTEST) += kfuzztest.o -kfuzztest-objs := kfuzztest_main.o relocations.o +kfuzztest-objs := kfuzztest_main.o relocations.o parse.o diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c new file mode 100644 index 00000000000000..3378113a0a370b --- /dev/null +++ b/lib/kfuzztest/parse.c @@ -0,0 +1,51 @@ +#include + +int __kfuzztest_parse_input(void *input, size_t input_size, + struct reloc_region_array **ret_regions, + struct reloc_table **ret_reloc_table, + void **ret_payload_start, void **ret_payload_end) +{ + int err; + void *payload_end, *payload_start; + size_t reloc_entries_size, regions_size; + struct reloc_table *rt; + struct reloc_region_array *regions; + + if (input_size < + sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) + return -EINVAL; + + payload_end = (char *)input + input_size; + + regions = input; + regions_size = sizeof(struct reloc_region_array) + + regions->num_regions * sizeof(struct reloc_region); + + rt = (struct reloc_table *)((char *)regions + regions_size); + if ((char *)rt > (char *)payload_end) { + err = -EINVAL; + goto fail; + } + + reloc_entries_size = sizeof(struct reloc_table) + + rt->num_entries * sizeof(struct reloc_entry); + if ((char *)rt + reloc_entries_size > (char *)payload_end) { + err = -EINVAL; + goto fail; + } + + payload_start = (char *)(rt->entries + rt->num_entries); + if ((char *)payload_start > (char *)payload_end) { + err = -EINVAL; + goto fail; + } + + *ret_regions = regions; + *ret_reloc_table = rt; + *ret_payload_start = payload_start; + *ret_payload_end = payload_end; + return 0; +fail: + kfree(input); + return err; +} diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index c2a2de1254b5e1..f373e75324582e 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -16,6 +16,8 @@ __kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, if (allocations[i]) kfree(allocations[i]); kfree((void *)handle); + /* Regions points to the beginning of the user buffer. */ + kfree((void *)regions); } void __kfuzztest_release_relocated(struct reloc_region_array *regions, @@ -33,10 +35,12 @@ void __kfuzztest_release_relocated(struct reloc_region_array *regions, } } +#define POISON_REGION_END 0xFC + static reloc_handle_t __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret) + struct reloc_table *rt, void *payload_start, + void *payload_end, void **data_ret) { size_t i; struct reloc_region reg, src, dst; @@ -44,32 +48,40 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, struct reloc_entry re; void *ptr; - /* Poison the padding between regions */ - for (i = 0; i < regions->num_regions; i++) { - reg = regions->regions[i]; - ptr = payload + reg.start + reg.size; - // TODO: poison this range. Figure out import for kasan.h. - } - - // TODO: failure when the region points out of bounds. - - /* Patch pointers */ + /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; + if (re.region_id > regions->num_regions) + goto fail; + src = regions->regions[re.region_id]; - ptr_location = (uintptr_t *)(char *)payload + src.start + + ptr_location = (uintptr_t *)(char *)payload_start + src.start + re.region_offset; + if ((char *)ptr_location >= (char *)payload_end) + goto fail; - if (re.value == nullPtr) { + if (re.value == KFUZZTEST_REGIONID_NULL) { *ptr_location = (uintptr_t)NULL; } else { if (re.value >= regions->num_regions) goto fail; dst = regions->regions[re.value]; - *ptr_location = (uintptr_t)(char *)payload + dst.start; + *ptr_location = + (uintptr_t)(char *)payload_start + dst.start; } } + /* Poison the padding between regions. */ + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + /* Points to the 8 bytes of padding following every region. */ + ptr = payload_start + reg.start + reg.size; + if ((char *)ptr + 8 >= (char *)payload_end) + goto fail; + kasan_poison(ptr, 8, POISON_REGION_END, false); + } + + /* Returned as `reloc_handle_t`. */ return regions; fail: __kfuzztest_release_relocated_poisoned(regions); @@ -78,8 +90,8 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, static reloc_handle_t __kfuzztest_relocate_distinct(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret) + struct reloc_table *rt, void *payload_start, + void *payload_end, void **data_ret) { void **allocated_regions; size_t i; @@ -94,36 +106,34 @@ __kfuzztest_relocate_distinct(struct reloc_region_array *regions, for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; + if (reg.size > KMALLOC_MAX_SIZE) + goto fail; /* kzalloc guarantees 8-byte alignment, which is enough. */ allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); if (!allocated_regions[i]) goto fail; - pr_info("copying from %px to %px with size 0x%x", - (char *)payload + reg.start, allocated_regions[i], - reg.size); - - memcpy(allocated_regions[i], (char *)payload + reg.start, + memcpy(allocated_regions[i], (char *)payload_start + reg.start, reg.size); - - pr_info("KFuzzTest: allocated region_%zu of size %u\n", i, - reg.size); } for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; + if (re.value >= regions->num_regions) + goto fail; + ptr_location = (uintptr_t *)((char *)allocated_regions[re.region_id] + re.region_offset); - if (re.value == nullPtr) { + + if ((char *)ptr_location >= (char *)payload_end) + goto fail; + + if (re.value == KFUZZTEST_REGIONID_NULL) *ptr_location = (uintptr_t)NULL; - } else { + else *ptr_location = (uintptr_t)allocated_regions[re.value]; - pr_info("KFuzzTest: pointer at offset %u in region %u pointer to region %u (0x%px)", - re.region_offset, re.region_id, re.value, - (void *)*ptr_location); - } } if (data_ret) @@ -131,26 +141,21 @@ __kfuzztest_relocate_distinct(struct reloc_region_array *regions, return allocated_regions; fail: - if (!allocated_regions) - return NULL; - for (i = 0; i < regions->num_regions; i++) { - if (allocated_regions[i]) - kfree(allocated_regions[i]); - } + __kfuzztest_release_relocated_distinct(regions, allocated_regions); return NULL; } reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload, - void **data_ret) + struct reloc_table *rt, void *payload_start, + void *payload_end, void **data_ret) { switch (regions->mode) { case DISTINCT: - return __kfuzztest_relocate_distinct(regions, rt, payload, - data_ret); + return __kfuzztest_relocate_distinct(regions, rt, payload_start, + payload_end, data_ret); case POISONED: - return __kfuzztest_relocate_poisoned(regions, rt, payload, - data_ret); + return __kfuzztest_relocate_poisoned(regions, rt, payload_start, + payload_end, data_ret); default: return ERR_PTR(-EINVAL); } diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h index 129178be5e6492..45f67188621cc4 100644 --- a/mm/kasan/kasan.h +++ b/mm/kasan/kasan.h @@ -499,15 +499,6 @@ static inline bool kasan_byte_accessible(const void *addr) #else /* CONFIG_KASAN_HW_TAGS */ -/** - * kasan_poison - mark the memory range as inaccessible - * @addr: range start address, must be aligned to KASAN_GRANULE_SIZE - * @size: range size, must be aligned to KASAN_GRANULE_SIZE - * @value: value that's written to metadata for the range - * @init: whether to initialize the memory range (only for hardware tag-based) - */ -void kasan_poison(const void *addr, size_t size, u8 value, bool init); - /** * kasan_unpoison - mark the memory range as accessible * @addr: range start address, must be aligned to KASAN_GRANULE_SIZE From 624a56639f2ceef79b341ecfe967c774034e57c1 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 09:01:40 +0000 Subject: [PATCH 06/41] kfuzztest: add toy example --- lib/kfuzztest/kfuzztest_main.c | 2 ++ lib/kfuzztest/kfuzzztest_examples.h | 39 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 lib/kfuzztest/kfuzzztest_examples.h diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index f0adc62c9882ad..7d9ef5c1ce30f2 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -11,6 +11,8 @@ #include #include +#include "kfuzzztest_examples.h" + extern const struct kfuzztest_target __kfuzztest_targets_start[]; extern const struct kfuzztest_target __kfuzztest_targets_end[]; diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzzztest_examples.h new file mode 100644 index 00000000000000..34fde1377eb2ef --- /dev/null +++ b/lib/kfuzztest/kfuzzztest_examples.h @@ -0,0 +1,39 @@ +#ifndef KFUZZTEST_EXAMPLES_H +#define KFUZZTEST_EXAMPLES_H + +#include + +struct nested_buffers { + const char *a; + size_t a_len; + const char *b; + size_t b_len; +}; + +/** + * The KFuzzTest input format specifies that struct nested buffers should + * be expanded as: + * + * | a | b | pad[8] | *a | pad[8] | *b | + * + * In DISTINCT mode, this will result in 3 distinct kmalloc'd regions, and + * in POISONED mode, the buffer is untouched but the padding will be poisoned. + * + * In this test case, we look to see that a KASAN warning is triggered in both + * cases when overflowing on *a by one. + */ +FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) +{ + KFUZZTEST_EXPECT_NOT_NULL(some_input, a); + KFUZZTEST_EXPECT_NOT_NULL(some_input, b); + KFUZZTEST_ANNOTATE_LEN(some_input, a_len, a); + KFUZZTEST_ANNOTATE_LEN(some_input, b_len, b); + + volatile char c; + pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); + /* Buffer overflow out of a bounds. This should be caught by KASAN. */ + for (size_t i = 0; i <= arg->a_len; i++) + c = arg->a[i]; +} + +#endif /* KFUZZTEST_EXAMPLES_H */ From 0ccd3a390e8afe95c1563d45fe435e0e1e811049 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 13:55:41 +0000 Subject: [PATCH 07/41] kfuzztest: backup before only supporting poisoned mode We are going to remove distinct mode in favor of only poisoning the padding areas between payload regions. --- include/linux/kfuzztest.h | 8 +++----- lib/kfuzztest/parse.c | 14 +++++++++++--- lib/kfuzztest/relocations.c | 34 +++++++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index dbe60f9533a5d4..060952fa42d056 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -14,12 +14,9 @@ struct reloc_region { uint32_t padding; }; -enum reloc_mode { DISTINCT = 0, POISONED }; - struct reloc_region_array { uint32_t num_regions; - enum reloc_mode mode; - uint32_t padding[2]; + uint32_t padding[3]; struct reloc_region regions[]; }; @@ -36,7 +33,8 @@ struct reloc_entry { struct reloc_table { uint32_t num_entries; - uint32_t padding[3]; + uint32_t payloadOffset; /* Offset from start of relocation table */ + uint32_t padding[2]; struct reloc_entry entries[]; }; static_assert(offsetof(struct reloc_table, entries) % diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 3378113a0a370b..720c9a970435e7 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -5,6 +5,7 @@ int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_table **ret_reloc_table, void **ret_payload_start, void **ret_payload_end) { + pr_info("[ENTER] %s", __FUNCTION__); int err; void *payload_end, *payload_start; size_t reloc_entries_size, regions_size; @@ -18,28 +19,35 @@ int __kfuzztest_parse_input(void *input, size_t input_size, payload_end = (char *)input + input_size; regions = input; - regions_size = sizeof(struct reloc_region_array) + + regions_size = sizeof(*regions) + regions->num_regions * sizeof(struct reloc_region); + pr_info("kfuzztest: num regions = %u", regions->num_regions); + rt = (struct reloc_table *)((char *)regions + regions_size); if ((char *)rt > (char *)payload_end) { err = -EINVAL; goto fail; } - reloc_entries_size = sizeof(struct reloc_table) + - rt->num_entries * sizeof(struct reloc_entry); + reloc_entries_size = + sizeof(*rt) + rt->num_entries * sizeof(struct reloc_entry); if ((char *)rt + reloc_entries_size > (char *)payload_end) { err = -EINVAL; goto fail; } + pr_info("kfuzztest: num relocations = %u, size = %zu", rt->num_entries, + reloc_entries_size); + payload_start = (char *)(rt->entries + rt->num_entries); if ((char *)payload_start > (char *)payload_end) { err = -EINVAL; goto fail; } + pr_info("kfuzztest: payload: [ %px, %px )", payload_start, payload_end); + *ret_regions = regions; *ret_reloc_table = rt; *ret_payload_start = payload_start; diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index f373e75324582e..a1171e61da059b 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -42,6 +42,7 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end, void **data_ret) { + pr_info("[ENTER] %s", __FUNCTION__); size_t i; struct reloc_region reg, src, dst; uintptr_t *ptr_location; @@ -51,14 +52,17 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; - if (re.region_id > regions->num_regions) - goto fail; + if (re.region_id >= regions->num_regions) + goto fail; src = regions->regions[re.region_id]; - ptr_location = (uintptr_t *)(char *)payload_start + src.start + - re.region_offset; + + ptr_location = (uintptr_t *)((char *)payload_start + src.start + + re.region_offset); if ((char *)ptr_location >= (char *)payload_end) goto fail; + if (src.start >= src.size) + goto fail; if (re.value == KFUZZTEST_REGIONID_NULL) { *ptr_location = (uintptr_t)NULL; @@ -67,20 +71,32 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, goto fail; dst = regions->regions[re.value]; *ptr_location = - (uintptr_t)(char *)payload_start + dst.start; + (uintptr_t)((char *)payload_start + dst.start); } } /* Poison the padding between regions. */ for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; + + pr_info("kfuzztest: region starts @ %px, with size %x", + payload_start + reg.start, reg.size); /* Points to the 8 bytes of padding following every region. */ ptr = payload_start + reg.start + reg.size; if ((char *)ptr + 8 >= (char *)payload_end) goto fail; - kasan_poison(ptr, 8, POISON_REGION_END, false); + + kasan_poison((char *)payload_start + reg.start, 8, + POISON_REGION_END, false); + + /* Currently only works if the length of the data is a multiple + * of 8. */ + void *next_boundary = (void *)round_up((uintptr_t)ptr, 8); + kasan_poison(next_boundary, 8, POISON_REGION_END, false); + pr_info("kfuzztest: poisoned %px", next_boundary); } + *data_ret = payload_start; /* Returned as `reloc_handle_t`. */ return regions; fail: @@ -93,6 +109,7 @@ __kfuzztest_relocate_distinct(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end, void **data_ret) { + pr_info("[ENTER] %s", __FUNCTION__); void **allocated_regions; size_t i; struct reloc_region reg; @@ -124,7 +141,7 @@ __kfuzztest_relocate_distinct(struct reloc_region_array *regions, goto fail; ptr_location = - (uintptr_t *)((char *)allocated_regions[re.region_id] + + (uintptr_t *)((char *)(allocated_regions[re.region_id]) + re.region_offset); if ((char *)ptr_location >= (char *)payload_end) @@ -136,8 +153,7 @@ __kfuzztest_relocate_distinct(struct reloc_region_array *regions, *ptr_location = (uintptr_t)allocated_regions[re.value]; } - if (data_ret) - *data_ret = allocated_regions[0]; + *data_ret = allocated_regions[0]; return allocated_regions; fail: From 614ce927f09ccc74f882fcdebc7de1af8736df68 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 18:08:58 +0000 Subject: [PATCH 08/41] kfuzztest: relocate in poison mode - the new only mode --- include/linux/kfuzztest.h | 31 +++-- lib/kfuzztest/kfuzzztest_examples.h | 3 + lib/kfuzztest/relocations.c | 179 +++++++++------------------- 3 files changed, 72 insertions(+), 141 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 060952fa42d056..5816ee03f2c518 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -73,13 +73,6 @@ reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end, void **data_ret); -/** - * Release the relocated data, freeing the initial buffer that was copied from - * user space. - */ -void __kfuzztest_release_relocated(struct reloc_region_array *regions, - reloc_handle_t handle); - struct kfuzztest_target { const char *name; const char *arg_type_name; @@ -172,24 +165,28 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, else if (IS_ERR(buffer)) \ return PTR_ERR(buffer); \ err = write_input_cb_common(filp, buf, len, off, buffer, len); \ - if (err != 0) { \ - kfree(buffer); \ - return err; \ - } \ + if (err != 0) \ + goto fail; \ err = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ &payload_start, &payload_end); \ if (err) \ - return err; \ + goto fail; \ /* Frees `buffer` on failure. */ \ relocated = __kfuzztest_relocate(regions, rt, payload_start, \ payload_end, (void *)&arg); \ - if (!relocated) \ - return -EINVAL; \ - /* Call the user's logic on the provided written input. */ \ + if (!relocated) { \ + err = -EINVAL; \ + goto fail; \ + } \ + pr_info("kfuzztest: success, invoking fuzz logic\n"); \ + /* Call the fuzz logic on the provided written input. */ \ _fuzz_test_logic_##test_name(arg); \ - /* Frees `buffer`. */ \ - __kfuzztest_release_relocated(regions, relocated); \ + kfree(buffer); \ return len; \ +fail: \ + pr_info("kfuzztest: a failure occured"); \ + kfree(buffer); \ + return err; \ } \ static void _fuzz_test_logic_##test_name(test_arg_type *arg) diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzzztest_examples.h index 34fde1377eb2ef..0209c779e9958a 100644 --- a/lib/kfuzztest/kfuzzztest_examples.h +++ b/lib/kfuzztest/kfuzzztest_examples.h @@ -31,6 +31,9 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) volatile char c; pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); + pr_info("b = [%px, %px)", arg->b, arg->b + arg->b_len); + pr_info("a_len = %zu", arg->a_len); + pr_info("b_len = %zu", arg->b_len); /* Buffer overflow out of a bounds. This should be caught by KASAN. */ for (size_t i = 0; i <= arg->a_len; i++) c = arg->a[i]; diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index a1171e61da059b..707d87d66616fa 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -1,53 +1,52 @@ #include #include -static void __kfuzztest_release_relocated_poisoned(reloc_handle_t handle) -{ - kfree(handle); -} - -static void -__kfuzztest_release_relocated_distinct(struct reloc_region_array *regions, - reloc_handle_t handle) -{ - size_t i; - void **allocations = (void **)handle; - for (i = 0; i < regions->num_regions; i++) - if (allocations[i]) - kfree(allocations[i]); - kfree((void *)handle); - /* Regions points to the beginning of the user buffer. */ - kfree((void *)regions); -} +#define POISON_REGION_END 0xFC -void __kfuzztest_release_relocated(struct reloc_region_array *regions, - reloc_handle_t handle) +static void __kfuzztest_poison_range(void *start, void *end) { - switch (regions->mode) { - case DISTINCT: - __kfuzztest_release_relocated_distinct(regions, handle); - break; - case POISONED: - __kfuzztest_release_relocated_poisoned(handle); - break; - default: - pr_warn("KFuzzTest: invalid release operation"); - } + uintptr_t start_addr = (uintptr_t)start; + uintptr_t end_addr = (uintptr_t)end; + uintptr_t poison_start; + uintptr_t poison_end; + + /* + * Calculate the largest region within [start, end) that is aligned + * to KASAN_GRANULE_SIZE. This is the only part we can safely poison. + */ + poison_start = ALIGN(start_addr, 0x8); + poison_end = ALIGN_DOWN(end_addr, 0x8); + + /* If there's no fully-aligned granule in the range, we can't do anything. */ + if (poison_start >= poison_end) + return; + + /* + * Poison the aligned region. KASAN_SLAB_REDZONE is a suitable + * poison value for padding that should never be accessed. + */ + kasan_poison((void *)poison_start, poison_end - poison_start, + POISON_REGION_END, false); + pr_info("kfuzztest: poisoned [%px, %px)", (void *)poison_start, + (void *)poison_end); } -#define POISON_REGION_END 0xFC - -static reloc_handle_t -__kfuzztest_relocate_poisoned(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end, void **data_ret) +reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload_start, + void *payload_end, void **data_ret) { pr_info("[ENTER] %s", __FUNCTION__); size_t i; struct reloc_region reg, src, dst; uintptr_t *ptr_location; struct reloc_entry re; - void *ptr; + void *poison_start, *poison_end; + + pr_info("kfuzztest: %d regions, %d relocations", regions->num_regions, + rt->num_entries); + + pr_info("kfuzztest: regions = %px, rt = %px, payload_start = %px, payload_end = %px", + regions, rt, payload_start, payload_end); /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { @@ -65,6 +64,7 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, goto fail; if (re.value == KFUZZTEST_REGIONID_NULL) { + pr_info("%px = NULL", ptr_location); *ptr_location = (uintptr_t)NULL; } else { if (re.value >= regions->num_regions) @@ -72,6 +72,8 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, dst = regions->regions[re.value]; *ptr_location = (uintptr_t)((char *)payload_start + dst.start); + pr_info("%px -> %px", ptr_location, + (void *)*ptr_location); } } @@ -79,100 +81,29 @@ __kfuzztest_relocate_poisoned(struct reloc_region_array *regions, for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; - pr_info("kfuzztest: region starts @ %px, with size %x", - payload_start + reg.start, reg.size); - /* Points to the 8 bytes of padding following every region. */ - ptr = payload_start + reg.start + reg.size; - if ((char *)ptr + 8 >= (char *)payload_end) - goto fail; + pr_info("kfuzztest: region %zu [%px, %px) (size = %u)", i, + payload_start + reg.start, + payload_start + reg.start + reg.size, reg.size); + /* Points to the beginning of the inter-region padding */ + poison_start = payload_start + reg.start + reg.size; + if (i < regions->num_regions - 1) { + poison_end = + payload_start + regions->regions[i + 1].start; + } else { + poison_end = poison_start + 8; + } - kasan_poison((char *)payload_start + reg.start, 8, - POISON_REGION_END, false); + if ((char *)poison_end > (char *)payload_end) { + pr_info("kfuzztest: poison region out of bounds"); + goto fail; + } - /* Currently only works if the length of the data is a multiple - * of 8. */ - void *next_boundary = (void *)round_up((uintptr_t)ptr, 8); - kasan_poison(next_boundary, 8, POISON_REGION_END, false); - pr_info("kfuzztest: poisoned %px", next_boundary); + __kfuzztest_poison_range(poison_start, poison_end); } *data_ret = payload_start; /* Returned as `reloc_handle_t`. */ return regions; fail: - __kfuzztest_release_relocated_poisoned(regions); - return NULL; -} - -static reloc_handle_t -__kfuzztest_relocate_distinct(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end, void **data_ret) -{ - pr_info("[ENTER] %s", __FUNCTION__); - void **allocated_regions; - size_t i; - struct reloc_region reg; - struct reloc_entry re; - uintptr_t *ptr_location; - - allocated_regions = - kzalloc(regions->num_regions * sizeof(void *), GFP_KERNEL); - if (!allocated_regions) - return NULL; - - for (i = 0; i < regions->num_regions; i++) { - reg = regions->regions[i]; - if (reg.size > KMALLOC_MAX_SIZE) - goto fail; - - /* kzalloc guarantees 8-byte alignment, which is enough. */ - allocated_regions[i] = kzalloc(reg.size, GFP_KERNEL); - if (!allocated_regions[i]) - goto fail; - - memcpy(allocated_regions[i], (char *)payload_start + reg.start, - reg.size); - } - - for (i = 0; i < rt->num_entries; i++) { - re = rt->entries[i]; - if (re.value >= regions->num_regions) - goto fail; - - ptr_location = - (uintptr_t *)((char *)(allocated_regions[re.region_id]) + - re.region_offset); - - if ((char *)ptr_location >= (char *)payload_end) - goto fail; - - if (re.value == KFUZZTEST_REGIONID_NULL) - *ptr_location = (uintptr_t)NULL; - else - *ptr_location = (uintptr_t)allocated_regions[re.value]; - } - - *data_ret = allocated_regions[0]; - return allocated_regions; - -fail: - __kfuzztest_release_relocated_distinct(regions, allocated_regions); return NULL; } - -reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end, void **data_ret) -{ - switch (regions->mode) { - case DISTINCT: - return __kfuzztest_relocate_distinct(regions, rt, payload_start, - payload_end, data_ret); - case POISONED: - return __kfuzztest_relocate_poisoned(regions, rt, payload_start, - payload_end, data_ret); - default: - return ERR_PTR(-EINVAL); - } -} From 62053d4f306a419678aa82115195de071ec22682 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 5 Aug 2025 19:08:28 +0000 Subject: [PATCH 09/41] kfuzztest: update poisoning to use kasan_poison_last_granule Also move functions out of mm/kasan/kasan.h into include/linux/kasan.h --- include/linux/kasan.h | 21 ++++++++++ include/linux/kfuzztest.h | 14 ++++--- lib/kfuzztest/kfuzzztest_examples.h | 10 ++++- lib/kfuzztest/relocations.c | 61 +++++++++++++++++++---------- mm/kasan/kasan.h | 19 --------- 5 files changed, 78 insertions(+), 47 deletions(-) diff --git a/include/linux/kasan.h b/include/linux/kasan.h index d6dba15bc57555..345ab363c7ba3e 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -126,6 +126,27 @@ static __always_inline bool kasan_unpoison_pages(struct page *page, return false; } +#ifdef CONFIG_KASAN_GENERIC + +/** + * kasan_poison_last_granule - mark the last granule of the memory range as + * inaccessible + * @address: range start address, must be aligned to KASAN_GRANULE_SIZE + * @size: range size + * + * This function is only available for the generic mode, as it's the only mode + * that has partially poisoned memory granules. + */ +void kasan_poison_last_granule(const void *address, size_t size); + +#else /* CONFIG_KASAN_GENERIC */ + +static inline void kasan_poison_last_granule(const void *address, size_t size) +{ +} + +#endif /* CONFIG_KASAN_GENERIC */ + void kasan_poison(const void *addr, size_t size, u8 value, bool init); void __kasan_poison_slab(struct slab *slab); diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 5816ee03f2c518..ecfa43bf19bcd3 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -141,7 +141,8 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, static ssize_t _write_callback_##test_name(struct file *filp, \ const char __user *buf, \ size_t len, loff_t *off); \ - static void _fuzz_test_logic_##test_name(test_arg_type *arg); \ + static void _fuzz_test_logic_##test_name( \ + test_arg_type *arg, struct reloc_region_array *regions); \ const struct kfuzztest_target __fuzz_test__##test_name __attribute__(( \ __section__(".kfuzztest_target"), __used__)) = { \ .name = #test_name, \ @@ -180,7 +181,7 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, } \ pr_info("kfuzztest: success, invoking fuzz logic\n"); \ /* Call the fuzz logic on the provided written input. */ \ - _fuzz_test_logic_##test_name(arg); \ + _fuzz_test_logic_##test_name(arg, regions); \ kfree(buffer); \ return len; \ fail: \ @@ -188,7 +189,8 @@ fail: \ kfree(buffer); \ return err; \ } \ - static void _fuzz_test_logic_##test_name(test_arg_type *arg) + static void _fuzz_test_logic_##test_name( \ + test_arg_type *arg, struct reloc_region_array *regions) /** * Reports a bug with a predictable prefix so that it can be parsed by a @@ -354,8 +356,8 @@ struct kfuzztest_annotation { */ #define KFUZZTEST_REGIONID_NULL U32_MAX -/* Performs some input validation, and returns the rerloc region array, and - * reloc table. - */ +/* The size of a region if it exists, or 0 if it does not. */ +#define KFUZZTEST_REGION_SIZE(n) \ + ((n) < (regions->num_regions) ? (regions->regions[n].size) : 0) #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzzztest_examples.h index 0209c779e9958a..65c6deda8e1915 100644 --- a/lib/kfuzztest/kfuzzztest_examples.h +++ b/lib/kfuzztest/kfuzzztest_examples.h @@ -24,6 +24,9 @@ struct nested_buffers { */ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) { + u32 a_size; + u32 b_size; + KFUZZTEST_EXPECT_NOT_NULL(some_input, a); KFUZZTEST_EXPECT_NOT_NULL(some_input, b); KFUZZTEST_ANNOTATE_LEN(some_input, a_len, a); @@ -34,8 +37,13 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) pr_info("b = [%px, %px)", arg->b, arg->b + arg->b_len); pr_info("a_len = %zu", arg->a_len); pr_info("b_len = %zu", arg->b_len); + + a_size = KFUZZTEST_REGION_SIZE(1); + b_size = KFUZZTEST_REGION_SIZE(2); + pr_info("actual sizes = %u, %u", a_size, b_size); + /* Buffer overflow out of a bounds. This should be caught by KASAN. */ - for (size_t i = 0; i <= arg->a_len; i++) + for (size_t i = 0; i <= a_size; i++) c = arg->a[i]; } diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 707d87d66616fa..c76d47584d2297 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -3,32 +3,51 @@ #define POISON_REGION_END 0xFC +/** + * Poison the half open interval [start, end], where end should be 8-byte + * aligned if it is not, then we cannot guarantee that the whole range will + * be poisoned. + * + * If start is not 8-byte-aligned, the remaining bytes in its 8-byte granule + * can only be poisoned if CONFIG_KASAN_GENERIC is enabled. + */ static void __kfuzztest_poison_range(void *start, void *end) { uintptr_t start_addr = (uintptr_t)start; - uintptr_t end_addr = (uintptr_t)end; - uintptr_t poison_start; - uintptr_t poison_end; - - /* - * Calculate the largest region within [start, end) that is aligned - * to KASAN_GRANULE_SIZE. This is the only part we can safely poison. - */ - poison_start = ALIGN(start_addr, 0x8); - poison_end = ALIGN_DOWN(end_addr, 0x8); - - /* If there's no fully-aligned granule in the range, we can't do anything. */ - if (poison_start >= poison_end) + uintptr_t end_addr = ALIGN_DOWN((uintptr_t)end, 0x8); + + uintptr_t poison_body_start; + uintptr_t poison_body_end; + uintptr_t head_granule_start; + size_t head_prefix_size; + + if (start_addr >= end_addr) return; - /* - * Poison the aligned region. KASAN_SLAB_REDZONE is a suitable - * poison value for padding that should never be accessed. - */ - kasan_poison((void *)poison_start, poison_end - poison_start, - POISON_REGION_END, false); - pr_info("kfuzztest: poisoned [%px, %px)", (void *)poison_start, - (void *)poison_end); + head_granule_start = ALIGN_DOWN(start_addr, 0x8); + head_prefix_size = start_addr - head_granule_start; + + if (IS_ENABLED(CONFIG_KASAN_GENERIC) && head_prefix_size > 0) { + kasan_poison_last_granule((void *)head_granule_start, + head_prefix_size); + pr_info("kfuzztest: poisoned [%px, %px)", + (void *)head_granule_start + head_prefix_size, + (void *)head_granule_start + 8); + } + + poison_body_start = ALIGN(start_addr, 0x8); + poison_body_end = ALIGN_DOWN(end_addr, 0x8); + + pr_info("kfuzztest: want to additionally poison [%px, %px)", + (void *)poison_body_start, (void *)poison_body_end); + if (poison_body_start < poison_body_end) { + kasan_poison((void *)poison_body_start, + poison_body_end - poison_body_start, + POISON_REGION_END, false); + + pr_info("kfuzztest: poisoned [%px, %px)", + (void *)poison_body_start, (void *)poison_body_end); + } } reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h index 45f67188621cc4..c4a478f2d22e42 100644 --- a/mm/kasan/kasan.h +++ b/mm/kasan/kasan.h @@ -516,25 +516,6 @@ bool kasan_byte_accessible(const void *addr); #endif /* CONFIG_KASAN_HW_TAGS */ -#ifdef CONFIG_KASAN_GENERIC - -/** - * kasan_poison_last_granule - mark the last granule of the memory range as - * inaccessible - * @address: range start address, must be aligned to KASAN_GRANULE_SIZE - * @size: range size - * - * This function is only available for the generic mode, as it's the only mode - * that has partially poisoned memory granules. - */ -void kasan_poison_last_granule(const void *address, size_t size); - -#else /* CONFIG_KASAN_GENERIC */ - -static inline void kasan_poison_last_granule(const void *address, size_t size) { } - -#endif /* CONFIG_KASAN_GENERIC */ - #ifndef kasan_arch_is_ready static inline bool kasan_arch_is_ready(void) { return true; } #elif !defined(CONFIG_KASAN_GENERIC) || !defined(CONFIG_KASAN_OUTLINE) From 5116dccf541652e54dc7537c5b4dc3ae8ab3879a Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 08:35:47 +0000 Subject: [PATCH 10/41] kfuzztest: cleanups WIP --- include/linux/kfuzztest.h | 102 ++++++++++------------------ lib/kfuzztest/kfuzztest_main.c | 14 ++-- lib/kfuzztest/kfuzzztest_examples.h | 8 +-- lib/kfuzztest/parse.c | 13 ++++ lib/kfuzztest/relocations.c | 47 ++++--------- 5 files changed, 71 insertions(+), 113 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index ecfa43bf19bcd3..120c8519971c9a 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -41,11 +41,9 @@ static_assert(offsetof(struct reloc_table, entries) % sizeof(struct reloc_entry) == 0); -/** - * Internal handle used for releasing resources, returned to the caller of - * __kfuzztest_relocate. - */ -typedef void *reloc_handle_t; +int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, + size_t len, loff_t *off, void *arg, + size_t arg_size); /** * Parses a binary input of size input_size. Input should be a pointer to a @@ -69,9 +67,9 @@ int __kfuzztest_parse_input(void *input, size_t input_size, /** * Relocates a parsed input into kernel memory. */ -reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end, void **data_ret); +int __kfuzztest_relocate(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload_start, + void *payload_end); struct kfuzztest_target { const char *name; @@ -82,19 +80,6 @@ struct kfuzztest_target { static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target should have size 32"); -__attribute__((unused)) 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((void *)arg, arg_size, off, buf, len) < 0) { - return -EFAULT; - } - return 0; -} - /** * FUZZ_TEST - defines a KFuzzTest target. * @@ -138,66 +123,56 @@ write_input_cb_common(struct file *filp, const char __user *buf, size_t len, * } */ #define FUZZ_TEST(test_name, test_arg_type) \ - static ssize_t _write_callback_##test_name(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off); \ - static void _fuzz_test_logic_##test_name( \ + static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, \ + const char __user *buf, \ + size_t len, \ + loff_t *off); \ + static void kfuzztest_logic_##test_name( \ test_arg_type *arg, struct reloc_region_array *regions); \ const struct kfuzztest_target __fuzz_test__##test_name __attribute__(( \ __section__(".kfuzztest_target"), __used__)) = { \ .name = #test_name, \ .arg_type_name = #test_arg_type, \ - .write_input_cb = _write_callback_##test_name, \ + .write_input_cb = kfuzztest_write_cb_##test_name, \ }; \ - /* Invoked when data is written into the target's input file. */ \ - static ssize_t _write_callback_##test_name(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off) \ + static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ { \ - int err; \ + int ret; \ struct reloc_region_array *regions; \ struct reloc_table *rt; \ - void *payload_start, *payload_end, *relocated; \ + void *payload_start, *payload_end, *buffer; \ test_arg_type *arg; \ \ - void *buffer = kmalloc(len, GFP_KERNEL); \ + buffer = kmalloc(len, GFP_KERNEL); \ if (!buffer) \ return -ENOMEM; \ else if (IS_ERR(buffer)) \ return PTR_ERR(buffer); \ - err = write_input_cb_common(filp, buf, len, off, buffer, len); \ - if (err != 0) \ - goto fail; \ - err = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ + ret = __kfuzztest_write_cb_common(filp, buf, len, off, buffer, \ + len); \ + if (ret) \ + goto out; \ + ret = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ &payload_start, &payload_end); \ - if (err) \ - goto fail; \ - /* Frees `buffer` on failure. */ \ - relocated = __kfuzztest_relocate(regions, rt, payload_start, \ - payload_end, (void *)&arg); \ - if (!relocated) { \ - err = -EINVAL; \ - goto fail; \ - } \ - pr_info("kfuzztest: success, invoking fuzz logic\n"); \ + if (ret) \ + goto out; \ + ret = __kfuzztest_relocate(regions, rt, payload_start, \ + payload_end); \ + if (ret) \ + goto out; \ /* Call the fuzz logic on the provided written input. */ \ - _fuzz_test_logic_##test_name(arg, regions); \ - kfree(buffer); \ - return len; \ -fail: \ - pr_info("kfuzztest: a failure occured"); \ + arg = (test_arg_type *)payload_start; \ + kfuzztest_logic_##test_name(arg, regions); \ + ret = len; \ +out: \ kfree(buffer); \ - return err; \ + return ret; \ } \ - static void _fuzz_test_logic_##test_name( \ + static void kfuzztest_logic_##test_name( \ test_arg_type *arg, struct reloc_region_array *regions) -/** - * Reports a bug with a predictable prefix so that it can be parsed by a - * fuzzing driver. - */ -#define KFUZZTEST_REPORT_BUG(msg, fmt) pr_warn("bug: " #msg, fmt) - enum kfuzztest_constraint_type : uint8_t { EXPECT_EQ = 0, EXPECT_NE, @@ -348,11 +323,8 @@ struct kfuzztest_annotation { __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) /** - * The relocation table format encodes pointer values as a relative offset from - * the location of the pointer. A relative offset of zero could indicate that - * the pointer points to its own address, which is valid. We encode a null - * pointer as 0xFFFFFFFF as adding this value to any address would result in an - * overflow anyways, and is therefore invalid in any other circumstance. + * The input format is such that the value field is a region index. We reserve + * this value to encode a NULL pointer in the input. */ #define KFUZZTEST_REGIONID_NULL U32_MAX diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index 7d9ef5c1ce30f2..4d26d3df46b10b 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -57,13 +57,7 @@ struct kfuzztest_state { /* Global static variable to hold all state for the module. */ static struct kfuzztest_state st; -/** - * Default file permissions for the debugfs entries. - * 0222: World-writable for the 'input' file. - * - * XXX: should formally define what the permissions should be on these files. - */ -const umode_t kfuzztest_flags_w = 0222; +const umode_t KFUZZTEST_INPUT_PERMS = 0222; /** * kfuzztest_init - Initializes the debug filesystem for KFuzzTest. @@ -126,7 +120,7 @@ static int __init kfuzztest_init(void) .write = targ->write_input_cb, }; st.debugfs_state[i].input_dentry.dentry = debugfs_create_file( - "input", kfuzztest_flags_w, + "input", KFUZZTEST_INPUT_PERMS, st.debugfs_state[i].target_dir, NULL, &st.debugfs_state[i].input_dentry.fops); if (!st.debugfs_state[i].input_dentry.dentry) { @@ -137,7 +131,7 @@ static int __init kfuzztest_init(void) goto cleanup_failure; } - pr_info("KFuzzTest: registered target %s\n", targ->name); + pr_info("KFuzzTest: registered target %s", targ->name); } return 0; @@ -149,7 +143,7 @@ static int __init kfuzztest_init(void) static void __exit kfuzztest_exit(void) { - pr_info("KFuzzTest: exiting\n"); + pr_info("KFuzzTest: exiting"); if (!st.kfuzztest_dir) return; diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzzztest_examples.h index 65c6deda8e1915..ed6c1005947184 100644 --- a/lib/kfuzztest/kfuzzztest_examples.h +++ b/lib/kfuzztest/kfuzzztest_examples.h @@ -27,10 +27,10 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) u32 a_size; u32 b_size; - KFUZZTEST_EXPECT_NOT_NULL(some_input, a); - KFUZZTEST_EXPECT_NOT_NULL(some_input, b); - KFUZZTEST_ANNOTATE_LEN(some_input, a_len, a); - KFUZZTEST_ANNOTATE_LEN(some_input, b_len, b); + KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, a); + KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, b); + KFUZZTEST_ANNOTATE_LEN(nested_buffers, a_len, a); + KFUZZTEST_ANNOTATE_LEN(nested_buffers, b_len, b); volatile char c; pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 720c9a970435e7..31b74d8135a0d0 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -1,5 +1,18 @@ #include +int __kfuzztest_write_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((void *)arg, arg_size, off, buf, len) < 0) { + return -EFAULT; + } + return 0; +} + int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_region_array **ret_regions, struct reloc_table **ret_reloc_table, diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index c76d47584d2297..64669ed4ac17dd 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -11,11 +11,10 @@ * If start is not 8-byte-aligned, the remaining bytes in its 8-byte granule * can only be poisoned if CONFIG_KASAN_GENERIC is enabled. */ -static void __kfuzztest_poison_range(void *start, void *end) +static void kfuzztest_poison_range(void *start, void *end) { - uintptr_t start_addr = (uintptr_t)start; uintptr_t end_addr = ALIGN_DOWN((uintptr_t)end, 0x8); - + uintptr_t start_addr = (uintptr_t)start; uintptr_t poison_body_start; uintptr_t poison_body_end; uintptr_t head_granule_start; @@ -50,49 +49,38 @@ static void __kfuzztest_poison_range(void *start, void *end) } } -reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end, void **data_ret) +int __kfuzztest_relocate(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload_start, + void *payload_end) { - pr_info("[ENTER] %s", __FUNCTION__); size_t i; struct reloc_region reg, src, dst; uintptr_t *ptr_location; struct reloc_entry re; void *poison_start, *poison_end; - pr_info("kfuzztest: %d regions, %d relocations", regions->num_regions, - rt->num_entries); - - pr_info("kfuzztest: regions = %px, rt = %px, payload_start = %px, payload_end = %px", - regions, rt, payload_start, payload_end); - /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; if (re.region_id >= regions->num_regions) - goto fail; + return -EINVAL; src = regions->regions[re.region_id]; ptr_location = (uintptr_t *)((char *)payload_start + src.start + re.region_offset); if ((char *)ptr_location >= (char *)payload_end) - goto fail; + return -EINVAL; if (src.start >= src.size) - goto fail; - + return -EINVAL; if (re.value == KFUZZTEST_REGIONID_NULL) { - pr_info("%px = NULL", ptr_location); *ptr_location = (uintptr_t)NULL; } else { if (re.value >= regions->num_regions) - goto fail; + return -EINVAL; dst = regions->regions[re.value]; *ptr_location = (uintptr_t)((char *)payload_start + dst.start); - pr_info("%px -> %px", ptr_location, - (void *)*ptr_location); } } @@ -100,9 +88,6 @@ reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; - pr_info("kfuzztest: region %zu [%px, %px) (size = %u)", i, - payload_start + reg.start, - payload_start + reg.start + reg.size, reg.size); /* Points to the beginning of the inter-region padding */ poison_start = payload_start + reg.start + reg.size; if (i < regions->num_regions - 1) { @@ -112,17 +97,11 @@ reloc_handle_t __kfuzztest_relocate(struct reloc_region_array *regions, poison_end = poison_start + 8; } - if ((char *)poison_end > (char *)payload_end) { - pr_info("kfuzztest: poison region out of bounds"); - goto fail; - } + if ((char *)poison_end > (char *)payload_end) + return -EINVAL; - __kfuzztest_poison_range(poison_start, poison_end); + kfuzztest_poison_range(poison_start, poison_end); } - *data_ret = payload_start; - /* Returned as `reloc_handle_t`. */ - return regions; -fail: - return NULL; + return 0; } From 83dfd185fde7a438ada16158692635200fcf8923 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 09:30:46 +0000 Subject: [PATCH 11/41] kfuzztest: remove unnecessary padding from kfuzztest.h structs --- include/linux/kfuzztest.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 120c8519971c9a..e2657bc61d5e71 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -10,36 +10,24 @@ MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); struct reloc_region { uint32_t start; /* Offset from the start of the payload. */ uint32_t size; - uint32_t alignment; - uint32_t padding; }; struct reloc_region_array { uint32_t num_regions; - uint32_t padding[3]; struct reloc_region regions[]; }; -static_assert(offsetof(struct reloc_region_array, regions) % - sizeof(struct reloc_region) == - 0); - struct reloc_entry { uint32_t region_id; /* Region that pointer belongs to. */ uint32_t region_offset; /* Offset from the beginning of the region. */ uint32_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ - uint32_t padding; }; struct reloc_table { uint32_t num_entries; uint32_t payloadOffset; /* Offset from start of relocation table */ - uint32_t padding[2]; struct reloc_entry entries[]; }; -static_assert(offsetof(struct reloc_table, entries) % - sizeof(struct reloc_entry) == - 0); int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, size_t len, loff_t *off, void *arg, From 60f56f6318d0fa29cd374f150de8350d27b3733c Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 09:42:26 +0000 Subject: [PATCH 12/41] kfuzztest: add SPDX headers to all new files --- include/linux/kfuzztest.h | 6 +++--- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/kfuzzztest_examples.h | 1 + lib/kfuzztest/parse.c | 1 + lib/kfuzztest/relocations.c | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index e2657bc61d5e71..4bd6905dfda865 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -1,3 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0 */ #ifndef KFUZZTEST_H #define KFUZZTEST_H @@ -34,9 +35,8 @@ int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, size_t arg_size); /** - * Parses a binary input of size input_size. Input should be a pointer to a - * heap-allocated buffer, and it's ownership is transferred to this function - * on call. + * Parses a binary input of size input_size. + * * @input: a heap-allocated buffer (ownership transferred). * @input_size: the byte-length of input * @ret_regions: return pointer to the relocation region array diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index 60414722c0c073..a16caee9f66e9c 100644 --- a/lib/kfuzztest/Makefile +++ b/lib/kfuzztest/Makefile @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: GPL-2.0-only +# SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_KFUZZTEST) += kfuzztest.o kfuzztest-objs := kfuzztest_main.o relocations.o parse.o diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzzztest_examples.h index ed6c1005947184..f895e76b481af7 100644 --- a/lib/kfuzztest/kfuzzztest_examples.h +++ b/lib/kfuzztest/kfuzzztest_examples.h @@ -1,3 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0 */ #ifndef KFUZZTEST_EXAMPLES_H #define KFUZZTEST_EXAMPLES_H diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 31b74d8135a0d0..effd475b744bc5 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -1,3 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0 */ #include int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 64669ed4ac17dd..50b957be2e278a 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -1,3 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0 */ #include #include From 3e9815ebb1776aee45057e2a21ed1aa2e099b0b6 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 09:53:33 +0000 Subject: [PATCH 13/41] kfuzztest: add help section to KConfig.kfuzztest --- lib/Kconfig.kfuzztest | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/Kconfig.kfuzztest b/lib/Kconfig.kfuzztest index 0fb000fe305c6a..dd9047459e051c 100644 --- a/lib/Kconfig.kfuzztest +++ b/lib/Kconfig.kfuzztest @@ -1,5 +1,12 @@ # SPDX-License-Identifier: GPL-2.0-only config KFUZZTEST - bool "Enable Kernel Fuzz Testing Framework (KFuzzTest)" - depends on DEBUG_FS + bool "KFuzzTest - enable support for internal fuzz targets" + depends on DEBUG_FS && KASAN + help + Enables support for the kernel fuzz testing framework (KFuzzTest), an + interface for exposing internal kernel functions to a userspace fuzzing + engine. KFuzzTest targets are exposed via a debugfs interface that + accepts serialized userspace inputs, and is designed to make it easier + to fuzz deeply nested kernel code that is hard to reach from the system + call boundary. From cc985fe86a458a2638a4fb79fad0645b24e94576 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 11:02:12 +0000 Subject: [PATCH 14/41] kfuzztest: update comments and one code block refactored --- include/linux/kfuzztest.h | 292 ++++++++++++++++++++++++++---------- lib/kfuzztest/parse.c | 11 +- lib/kfuzztest/relocations.c | 8 +- 3 files changed, 219 insertions(+), 92 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 4bd6905dfda865..8de95a24f19187 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -8,25 +8,89 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ethan Graham "); MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); +/** + * @brief The KFuzzTest Input Serialization Format + * + * KFuzzTest receives its input from userspace as a single binary blob. This + * format allows for the serialization of complex, pointer-rich C structures + * into a flat buffer that can be safely passed into the kernel. This format + * requires only a single copy from userspace into a kenrel buffer, and no + * further kernel allocations. Pointers are patched internally using a "region" + * system where each region corresponds to some pointed-to data. + * + * Regions should be padded to respect alignment constraints of their + * underlying types. These padded regions are poisoned by KFuzzTest to ensure + * that KASAN catches OOB accesses. + * + * The format consists of three main components: + * 1. A reloc_region_array: Defines the memory layout of the target structure + * by partitioning the payload into logical regions. Each logical region + * should contain the byte representation of the type that it represents, + * including any necessary padding. + * 2. A reloc_table: Provides "linking" instructions that tell the kernel how + * to patch pointer fields to point to the correct regions. By design, + * the first region (index 0) is passed as input into a FUZZ_TEST. + * 3. A Payload: The raw binary data for the structure and its associated + * buffers. This should be aligned to the maximum alignment of all + * regions to satisfy alignment requirements of the input types, but this + * isn't checked by the parser. The payload should have at least 8 bytes + * of trailing padding. + * + * For a detailed specification of the binary layout see the full documentation + * at: Documentation/dev-tools/kfuzztest.rst + */ + +/** + * struct reloc_region - Describes a single contiguous memory region in the + * payload. + * + * @start: The byte offset of this region from the start of the payload, which + * should be aligned to the alignment requirements of the region's + * underlying type. + * @size: The size of this region in bytes. + */ struct reloc_region { - uint32_t start; /* Offset from the start of the payload. */ + uint32_t start; uint32_t size; }; +/** + * struct reloc_region_array - An array of all memory regions. + * @num_regions: The total number of regions defined. + * @regions: A flexible array of `num_regions` region descriptors. + */ struct reloc_region_array { uint32_t num_regions; struct reloc_region regions[]; }; +/** + * struct reloc_entry describes a single pointer to be patched. + * + * @region_id: The index of the region in the `reloc_region_array` that + * contains the pointer. + * @region_offset: The start offset of the pointer inside of the region. + * @value: contains the index of the pointee region, or KFUZZTEST_REGIONID_NULL + * if the pointer is NULL. + */ struct reloc_entry { - uint32_t region_id; /* Region that pointer belongs to. */ - uint32_t region_offset; /* Offset from the beginning of the region. */ - uint32_t value; /* Pointee tegion identifier, or (void*)-1 if NULL */ + uint32_t region_id; + uint32_t region_offset; + uint32_t value; }; +/** + * struct reloc_entry is an array of all pointer relocations required by an + * input. + * + * @num_entries: the number of pointer relocations. + * @payload_offset: the number of padded bytes between the last relocation in + * entries, and the start of the payload data. + * @entries: array of relocations. + */ struct reloc_table { uint32_t num_entries; - uint32_t payloadOffset; /* Offset from start of relocation table */ + uint32_t payload_offset; struct reloc_entry entries[]; }; @@ -35,17 +99,26 @@ int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, size_t arg_size); /** - * Parses a binary input of size input_size. + * __kfuzztest_parse_input validates and parses the KFuzzTest input format. + * + * @input: A buffer containing the serialized test case. + * @input_size: The size in bytes of the @input buffer. + * @ret_regions: On success, updated to point to the relocation region array + * within the @input buffer. + * @ret_reloc_table: On success, updated to point to the relocation table + * within the @input buffer. + * @ret_payload_start: On success, updated to point to the start of the data + * payload within the @input buffer. + * @ret_payload_end: On success, updated to point to the first byte after the + * end of the data payload. * - * @input: a heap-allocated buffer (ownership transferred). - * @input_size: the byte-length of input - * @ret_regions: return pointer to the relocation region array - * @ret_reloc_table: return pointer to the relocation table - * @ret_payload_start: return pointer to the start of payload the data - * @ret_payload_end: return pointer to the end of the payload data, i.e., the - * first address that is out of the bounds of the payload. + * This function performs the initial validation of the binary input blob. It + * checks that the declared sizes of the region array and relocation table are + * within the bounds of the total input size and extracts pointers to the + * main components of the format. It does not perform a deep validation of + * the entries themselves, which is instead done by __kfuzztest_relocate. * - * @return 0 on success, or an error code. + * Returns: 0 on success, or a negative error code on failure. */ int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_region_array **ret_regions, @@ -53,7 +126,25 @@ int __kfuzztest_parse_input(void *input, size_t input_size, void **ret_payload_start, void **ret_payload_end); /** - * Relocates a parsed input into kernel memory. + * __kfuzztest_relocate hydrates the payload by patching pointers. + * + * @regions: The relocation region array parsed from the input. + * @rt: The relocation table parsed from the input. + * @payload_start: A pointer to the start of the data payload. + * @payload_end: A pointer to the first byte after the end of the payload. + * + * This function iterates through the region array and relocation table to + * patch the pointers inside of the payload, reconstructing pointer-pointee + * relationships between the logical regions of the fuzz driver input. For + * each entry in @rt, it calculates the address the address of a pointer field + * within the payload and sets it to the start address of its target region, or + * a NULL pointer if marked with KFUZZTEST_REGIONID_NULL. + * + * The padded areas between each region are poisoned with a KASAN slab redzone + * to enable the detection of byte-accurate OOB accesses in the fuzz logic. + * + * Returns: 0 on success, or a negative error code if the relocation data is + * found to be corrupt (e.g., invalid pointers). */ int __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, @@ -69,45 +160,64 @@ static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target should have size 32"); /** - * FUZZ_TEST - defines a KFuzzTest target. + * FUZZ_TEST defines a KFuzzTest target. + * + * @test_name: The unique identifier for the fuzz test, which is used to name + * the debugfs entry, e.g., /sys/kernel/debug/kftf/@test_name. + * @test_arg_type: The struct type that defines the inputs for the test. This + * must be the full struct type (e.g., "struct my_inputs"), not a typedef. * - * @test_name: Name of the fuzz target, which is used to create the associated - * debufs entries. - * @test_arg_type: the input type of fuzz target. This should always be a - * struct type even when fuzzing with a single input parameter in order - * to take advantage of the domain constraint and annotation systems. See - * usage example below. + * Context: + * This macro is the primary entry point for the KFuzzTest framework. It + * generates all the necessary boilerplate for a fuzz test, including: + * - A static `struct kfuzztest_target` instance that is placed in a + * dedicated ELF section for discovery by userspace tools. + * - A `debugfs` write callback that handles receiving serialized data from + * a fuzzer, parsing it, and "hydrating" it into a valid C struct. + * - A function stub where the developer places the test logic. * + * User-Provided Logic: + * The developer must provide the body of the fuzz test logic within the curly + * braces following the macro invocation. Within this scope, the framework + * provides the following variables: * - * This macro generates all of the necessary boilerplate for a KFuzzTest - * driver, which is placed in a dedicated ".kfuzztest_target" that is used by - * the KFuzzTest module and can be read by a fuzzing engine. + * - `arg`: A pointer of type `@test_arg_type *` to the fully hydrated input + * structure. All pointer fields within this struct have been relocated + * and are valid kernel pointers. This is the primary variable to use + * for accessing fuzzing inputs. * - * For each test, this macro generates - * - A buffer to receive input through the debugfs entry - * - A mutex to protect the input buffer - * - A `struct kfuzztest_target` instance + * - `regions`: A pointer of type `struct reloc_region_array *`. This is an + * advanced feature that allows access to the raw region metadata, which + * can be useful for checking the actual allocated size of a buffer via + * `KFUZZTEST_REGION_SIZE(n)`. * - * Example usage: + * Example Usage: * - * // Assume that we are fuzzing some function func(T1 param1, ... TN paramN). - * // Define input type of the fuzz target. This should be always be a struct. - * struct test_arg_type { - * T1 arg1; - * ... - * TN argn; + * // 1. The kernel function we want to fuzz. + * int process_data(const char *data, size_t len); + * + * // 2. Define a struct to hold all inputs for the function. + * struct process_data_inputs { + * const char *data; + * size_t len; * }; * - * // Define the test case. - * FUZZ_TEST(test_func, struct test_arg_type) + * // 3. Define the fuzz test using the FUZZ_TEST macro. + * FUZZ_TEST(process_data_fuzzer, struct process_data_inputs) * { - * int ret; - * // arg is provided by the macro, and is of type struct test_arg_type. - * ret = func(arg.arg1, ..., arg.argn); - * // Validate the return value if testing for correctness. - * if (ret != expected_value) { - * KFUZZTEST_REPORT_BUG("Unexpected return value"); - * } + * int ret; + * // Use KFUZZTEST_EXPECT_* to enforce preconditions. + * // The test will exit early if data is NULL. + * KFUZZTEST_EXPECT_NOT_NULL(process_data_inputs, data); + * + * // Use KFUZZTEST_ANNOTATE_* to provide hints to the fuzzer. + * // This links the 'len' field to the 'data' buffer. + * KFUZZTEST_ANNOTATE_LEN(process_data_inputs, len, data); + * + * // Call the function under test using the 'arg' variable. OOB memory + * // accesses will be caught by KASAN, but the user can also choose to + * // validate the return value and log any failures. + * ret = process_data(arg->data, arg->len); * } */ #define FUZZ_TEST(test_name, test_arg_type) \ @@ -170,37 +280,31 @@ enum kfuzztest_constraint_type : uint8_t { }; /** - * Domain constraints are used to restrict the values that the fuzz driver - * accepts, enforcing early exit when not satisfied. Domain constraints are - * encoded in vmlinux under the `__kfuzztest_constraint` section. A good - * fuzzing engine should be aware of these domain constraints during input - * generation and mutation. + * struct kfuzztest_constraint defines a metadata record for a domain + * constraint. * - * struct kfuzztest_constraint defines a domain constraint for a structure - * field. + * Domain constraints are rules about the input data that must be satisfied for + * a fuzz test to proceed. While they are enforced in the kernel with a runtime + * check, they are primarily intended as a discoverable contract for userspace + * fuzzers. * - * @input_type: the name of the input (a struct name) - * @field_name: the name of the field that this domain constraint applies to - * @value1: used in all comparisons - * @value2: only used in comparisons that require multiple values, e.g. range - * constraints - * @type: the type of the constraint, enumerated above + * Instances of this struct are generated by the KFUZZTEST_EXPECT_* macros + * and placed into the read-only ".kfuzztest_constraint" ELF section of the + * vmlinux binary. A fuzzer can parse this section to learn about the + * constraints and generate valid inputs more intelligently. * - * Example usage: + * For an example of how these constraints are used within a fuzz test, see the + * documentation for the FUZZ_TEST() macro. * - * struct foo { - * struct bar *a; - * int b - * }; - * - * FUZZ_TEST(test_name, struct foo) - * { - * // Early exit if foo.a == NULL. - * KFUZZTEST_EXPECT_NOT_NULL(foo, a); - * // Early exit if foo < 23 || foo > 42 - * KFUZZTEST_EXPECT_IN_RANGE(foo, b, 23, 42); - * // User-defined fuzz logic. - * } + * @input_type: The name of the input struct type, without the leading + * "struct ". + * @field_name: The name of the field within the struct that this constraint + * applies to. + * @value1: The primary value used in the comparison (e.g., the upper + * bound for EXPECT_LE). + * @value2: The secondary value, used only for multi-value comparisons + * (e.g., the upper bound for EXPECT_IN_RANGE). + * @type: The type of the constraint. */ struct kfuzztest_constraint { const char *input_type; @@ -259,13 +363,15 @@ static_assert(sizeof(struct kfuzztest_constraint) == 64, /** * Annotations express attributes about structure fields that can't be easily - * verified at runtime, and are intended as a hint to the fuzzing engine. + * or safely verified at runtime. They are intended as hints to the fuzzing + * engine to help it generate more semantically correct and effective inputs. + * Unlike constraints, annotations do not add any runtime checks and do not + * cause a test to exit early. * - * For example, a char* could either be a raw byte buffer or a string, where - * the latter is null terminated. If a function accepts a null-terminated - * string without a length and is passed an arbitrary byte buffer, we - * may get false positive KASAN reports, for example. However, verifying that - * the char buffer is null-termined could itself trigger a memory overflow. + * For example, a `char *` field could be a raw byte buffer or a C-style + * null-terminated string. A fuzzer that is aware of this distinction can avoid + * creating inputs that would cause trivial, uninteresting crashes from reading + * past the end of a non-null-terminated buffer. */ enum kfuzztest_annotation_attribute : uint8_t { ATTRIBUTE_LEN = 0, @@ -273,6 +379,30 @@ enum kfuzztest_annotation_attribute : uint8_t { ATTRIBUTE_ARRAY, }; +/** + * struct kfuzztest_annotation defines a metadata record for a fuzzer hint. + * + * This struct captures a single hint about a field in the input structure. + * Instances are generated by the KFUZZTEST_ANNOTATE_* macros and are placed + * into the read-only ".kfuzztest_annotation" ELF section of the vmlinux binary. + * + * A userspace fuzzer can parse this section to understand the semantic + * relationships between fields (e.g., which field is a length for which + * buffer) and the expected format of the data (e.g., a null-terminated + * string). This allows the fuzzer to be much more intelligent during input + * generation and mutation. + * + * For an example of how annotations are used within a fuzz test, see the + * documentation for the FUZZ_TEST() macro. + * + * @input_type: The name of the input struct type. + * @field_name: The name of the field being annotated (e.g., the data + * buffer field). + * @linked_field_name: For annotations that link two fields (like + * ATTRIBUTE_LEN), this is the name of the related field (e.g., the + * length field). For others, this may be unused. + * @attrib: The type of the annotation hint. + */ struct kfuzztest_annotation { const char *input_type; const char *field_name; @@ -310,13 +440,9 @@ struct kfuzztest_annotation { #define KFUZZTEST_ANNOTATE_LEN(arg_type, field, linked_field) \ __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) -/** - * The input format is such that the value field is a region index. We reserve - * this value to encode a NULL pointer in the input. - */ #define KFUZZTEST_REGIONID_NULL U32_MAX -/* The size of a region if it exists, or 0 if it does not. */ +/** Get the size of a payload region from within a FUZZ_TEST body */ #define KFUZZTEST_REGION_SIZE(n) \ ((n) < (regions->num_regions) ? (regions->regions[n].size) : 0) diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index effd475b744bc5..82f87bd0fcec3c 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -22,7 +22,7 @@ int __kfuzztest_parse_input(void *input, size_t input_size, pr_info("[ENTER] %s", __FUNCTION__); int err; void *payload_end, *payload_start; - size_t reloc_entries_size, regions_size; + size_t reloc_table_size, regions_size; struct reloc_table *rt; struct reloc_region_array *regions; @@ -44,15 +44,16 @@ int __kfuzztest_parse_input(void *input, size_t input_size, goto fail; } - reloc_entries_size = - sizeof(*rt) + rt->num_entries * sizeof(struct reloc_entry); - if ((char *)rt + reloc_entries_size > (char *)payload_end) { + reloc_table_size = sizeof(*rt) + + rt->num_entries * sizeof(struct reloc_entry) + + rt->payload_offset; + if ((char *)rt + reloc_table_size > (char *)payload_end) { err = -EINVAL; goto fail; } pr_info("kfuzztest: num relocations = %u, size = %zu", rt->num_entries, - reloc_entries_size); + reloc_table_size); payload_start = (char *)(rt->entries + rt->num_entries); if ((char *)payload_start > (char *)payload_end) { diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 50b957be2e278a..5636816f442b59 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -74,14 +74,15 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, return -EINVAL; if (src.start >= src.size) return -EINVAL; + if (re.value == KFUZZTEST_REGIONID_NULL) { *ptr_location = (uintptr_t)NULL; - } else { - if (re.value >= regions->num_regions) - return -EINVAL; + } else if (re.value < regions->num_regions) { dst = regions->regions[re.value]; *ptr_location = (uintptr_t)((char *)payload_start + dst.start); + } else { + return -EINVAL; } } @@ -103,6 +104,5 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, kfuzztest_poison_range(poison_start, poison_end); } - return 0; } From 4ad76d024c2d5324ad65b75179a26552a6e02299 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 11:07:05 +0000 Subject: [PATCH 15/41] kfuzztest: move examples to .c file --- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/{kfuzzztest_examples.h => kfuzztest_examples.c} | 0 lib/kfuzztest/kfuzztest_main.c | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) rename lib/kfuzztest/{kfuzzztest_examples.h => kfuzztest_examples.c} (100%) diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index a16caee9f66e9c..af7fd847b89e30 100644 --- a/lib/kfuzztest/Makefile +++ b/lib/kfuzztest/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_KFUZZTEST) += kfuzztest.o -kfuzztest-objs := kfuzztest_main.o relocations.o parse.o +kfuzztest-objs := kfuzztest_main.o relocations.o parse.o kfuzztest_examples.o diff --git a/lib/kfuzztest/kfuzzztest_examples.h b/lib/kfuzztest/kfuzztest_examples.c similarity index 100% rename from lib/kfuzztest/kfuzzztest_examples.h rename to lib/kfuzztest/kfuzztest_examples.c diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index 4d26d3df46b10b..af16cae8acdd35 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -11,8 +11,6 @@ #include #include -#include "kfuzzztest_examples.h" - extern const struct kfuzztest_target __kfuzztest_targets_start[]; extern const struct kfuzztest_target __kfuzztest_targets_end[]; From 14f3851519726349e3f1e2422fe947a463f2d7f9 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 12:00:16 +0000 Subject: [PATCH 16/41] kfuzztest: wrap KFUZZTEST_TABLE def in an ifdef block --- arch/x86/kernel/vmlinux.lds.S | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/arch/x86/kernel/vmlinux.lds.S b/arch/x86/kernel/vmlinux.lds.S index fdcca6b4b2f44e..484e3e1ffb9f9e 100644 --- a/arch/x86/kernel/vmlinux.lds.S +++ b/arch/x86/kernel/vmlinux.lds.S @@ -113,6 +113,7 @@ ASSERT(__relocate_kernel_end - __relocate_kernel_start <= KEXEC_CONTROL_CODE_MAX #define KEXEC_RELOCATE_KERNEL #endif +#ifdef CONFIG_KFUZZTEST #define KFUZZTEST_TABLE \ . = ALIGN(PAGE_SIZE); \ __kfuzztest_targets_start = .; \ @@ -125,7 +126,11 @@ ASSERT(__relocate_kernel_end - __relocate_kernel_start <= KEXEC_CONTROL_CODE_MAX . = ALIGN(PAGE_SIZE); \ __kfuzztest_annotations_start = .; \ KEEP(*(.kfuzztest_annotation)); \ - __kfuzztest_annotations_end = .; \ + __kfuzztest_annotations_end = .; + +#else /* CONFIG_KFUZZTEST */ +#define KFUZZTEST_TABLE +#endif /* CONFIG_KFUZZTEST */ PHDRS { text PT_LOAD FLAGS(5); /* R_E */ From bd5d83376476522e1860f35b73169e0c418d2729 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Wed, 6 Aug 2025 13:28:03 +0000 Subject: [PATCH 17/41] kfuzztest: start documentation kfuzztest: update documentation --- Documentation/dev-tools/kfuzztest.rst | 179 ++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 Documentation/dev-tools/kfuzztest.rst diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst new file mode 100644 index 00000000000000..ad21a4fa5ad40b --- /dev/null +++ b/Documentation/dev-tools/kfuzztest.rst @@ -0,0 +1,179 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. Copyright (C) 2025, Google LLC. + +Kernel Fuzz Testing Framework (KFuzzTest) +========================================= + +Overview +-------- + +The Kernel Fuzz Testing Framework (KFuzzTest) is a framework designed to expose +internal kernel functions to a userspace fuzzing engine. + +It is intended for testing stateless or low-state functions that are difficult +to reach from the system call interface, such as routines involved in file +format parsing or complex data transformations. This provides a method for +in-situ fuzzing of kernel code without requiring that it be built as a separate +userspace library or that its dependencies be stubbed out. + +The framework consists of four main components: + +1. An API, based on the ``FUZZ_TEST`` macro, for defining test targets + directly in the kernel tree. +2. A binary serialization format for passing complex, pointer-rich data + structures from userspace to the kernel. +3. A ``debugfs`` interface through which a userspace fuzzer submits + serialized test inputs. +4. Metadata embedded in dedicated ELF sections of the ``vmlinux`` binary to + allow for the discovery of available fuzz targets by external tooling. + +Supported Architectures +----------------------- + +KFuzzTest is currently only supported for x86_64. + +Usage +----- + +To enable KFuzzTest, configure the kernel with:: + + CONFIG_KFUZZTEST=y + +which depends on ``CONFIG_DEBUGFS`` for receiving userspace inputs. + +Declaring a KFuzzTest target +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A fuzz target is defined directly in a .c file, typically alongside the function +being tested. This process involves three main parts: defining an input +structure, writing the test body using the ``FUZZ_TEST`` macro, and optionally +adding metadata for the fuzzer. + +The following example illustrates how to create a fuzz target for a function +``int process_data(const char *data, size_t len).`` + +.. code-block:: c + + // 1. Define a struct to model the inputs for the function under test. + // Each field corresponds to an argument needed by the function. + struct process_data_inputs { + const char *data; + size_t len; + }; + + // 2. Define the fuzz target using the FUZZ_TEST macro. + // The first parameter is a unique name for the target. + // The second parameter is the input struct defined above. + FUZZ_TEST(test_process_data, struct process_data_inputs) + { + // Within this body, the 'arg' variable is a pointer to a + // fully initialized 'struct process_data_inputs'. + + // 3. (Optional) Add constraints to define preconditions. + // This check ensures 'arg->data' is not NULL. If the condition + // is not met, the test exits early. This also creates metadata + // to inform the fuzzer + KFUZZTEST_EXPECT_NOT_NULL(process_data_inputs, data); + + // 4. (Optional) Add annotations to provide semantic hints. + // This annotation informs the fuzzer that the 'len' field + // is the length of the buffer pointed to by 'data'. + // Annotations do not add any runtime checks. + KFUZZTEST_ANNOTATE_LEN(process_data_inputs, len, data); + + // 5. Call the kernel function with the provided inputs. + // Memory errors like out-of-bounds accesses on 'arg->data' + // will be detected by KASAN. + process_data(arg->data, arg->len); + } + +KFuzzTest provides two families of macros to improve the quality of fuzzing: + +- ``KFUZZTEST_EXPECT_*``: These macros define constraints, which are + preconditions that must be true for the test to proceed. They are enforced + with a runtime check in the kernel. If a check fails, the current test run is + aborted. This metadata helps the userspace fuzzer avoid generating invalid + inputs. + +- ``KFUZZTEST_ANNOTATE_*``: These macros define annotations, which are purely + semantic hints for the fuzzer. They do not add any runtime checks and exist + only to help the fuzzer generate more intelligent and structurally correct + inputs. For example, KFUZZTEST_ANNOTATE_LEN links a size field to a pointer + field, which is a common pattern in C APIs. + +Input Format +------------ + +KFuzzTest targets receive their inputs from userspace via a write to a dedicated +debugfs ``/sys/kernel/debug/kfuzztest//input``. + +The data written to this file must be a single binary blob that follows a +specific serialization format. This format is designed to allow complex, +pointer-rich C structures to be represented in a flat buffer, requiring only a +single kernel allocation and copy from userspace. + +The format consists of three main parts laid out sequentially: a region array, +a relocation table, and the payload.:: + + +----------------+---------------------+-----------------+----------------+ + | region array | relocation table | padding | payload | + +----------------+---------------------+-----------------+----------------+ + +Region Array +~~~~~~~~~~~~ + +This component is a header that describes how the raw data in the Payload is +partitioned into logical memory regions. It consists of a count of regions +followed by an array of ``struct reloc_region``, where each entry defines a +single region with its size and offset from the start of the payload. + +.. code-block:: c + + struct reloc_region { + uint32_t offset; + uint32_t size; + }; + + struct reloc_region_array { + uint32_t num_regions; + struct reloc_region regions[]; + }; + +By convention, region 0 represents the top-level input struct that is passed +as the arg variable to the FUZZ_TEST body. Subsequent regions typically +represent data buffers pointed to by fields within that struct. Region array +entries must be ordered by offset ascending, and must not overlap with one +another. + +Relocation Table +~~~~~~~~~~~~~~~~ + +The relocation table provides the instructions for the kernel to "hydrate" the +payload by patching pointer fields. It contains an array of struct reloc_entry +items. Each entry acts as a linking instruction, specifying: + +- The location of a pointer that needs to be patched (identified by a region + ID and an offset within that region). + +- The target region that the pointer should point to (identified by the + target's region ID) or ``KFUZZTEST_REGIONID_NULL`` if the pointer is ``NULL``. + +This table also specifies the amount of padding between its end and the start +of the payload, which should be at least 8 bytes. + +Payload +~~~~~~~ + +The payload contains the raw binary data for all regions, concatenated together +according to their specified offsets. + +- Alignment: The start of the payload must be aligned to the most restrictive + alignment requirement of all its constituent regions. The framework ensures + that each region within the payload is then placed at an offset that respects + its own type's alignment. + +- Padding and Poisoning: The space between the end of one region's data and the + beginning of the next must be sufficient for padding. KFuzzTest poisons this + unused padding with KASAN, allowing for precise detection of out-of-bounds + memory accesses between adjacent buffers. This padding should be at least + ``KFUZZTEST_POISON_SIZE`` bytes as defined in `include/linux/kfuzztest.h``. From f39fd771266db8432e171a0d53bb780570478865 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 09:14:01 +0000 Subject: [PATCH 18/41] kfuzztest: add Google copyright notice, remove ifdefs in .c file --- Documentation/dev-tools/kfuzztest.rst | 2 +- include/linux/kfuzztest.h | 3 ++- lib/kfuzztest/kfuzztest_examples.c | 8 ++------ lib/kfuzztest/kfuzztest_main.c | 4 +++- lib/kfuzztest/parse.c | 1 + lib/kfuzztest/relocations.c | 3 ++- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst index ad21a4fa5ad40b..1c8723a5b9b980 100644 --- a/Documentation/dev-tools/kfuzztest.rst +++ b/Documentation/dev-tools/kfuzztest.rst @@ -1,5 +1,5 @@ .. SPDX-License-Identifier: GPL-2.0 -.. Copyright (C) 2025, Google LLC. +.. Copyright 2025 Google LLC Kernel Fuzz Testing Framework (KFuzzTest) ========================================= diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 8de95a24f19187..4ec7813cfbea33 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -1,4 +1,5 @@ -/* SPDX-License-Identifier: GPL-2.0 */ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright 2025 Google LLC */ #ifndef KFUZZTEST_H #define KFUZZTEST_H diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index f895e76b481af7..58971edd4965c0 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -1,7 +1,5 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -#ifndef KFUZZTEST_EXAMPLES_H -#define KFUZZTEST_EXAMPLES_H - +// SPDX-License-Identifier: GPL-2.0 +/* Copyright 2025 Google LLC */ #include struct nested_buffers { @@ -47,5 +45,3 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) for (size_t i = 0; i <= a_size; i++) c = arg->a[i]; } - -#endif /* KFUZZTEST_EXAMPLES_H */ diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index af16cae8acdd35..766452b63ec449 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -1,10 +1,12 @@ -/* SPDX-License-Identifier: GPL-2.0 */ +// SPDX-License-Identifier: GPL-2.0 /* * Kernel Fuzz Testing Framework (KFuzzTest) - 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/kfuzztest/ for userspace to interact with each test. + * + * Copyright 2025 Google LLC */ #include #include diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 82f87bd0fcec3c..0aa4150ba52dc5 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -1,4 +1,5 @@ /* SPDX-License-Identifier: GPL-2.0 */ +/* Copyright 2025 Google LLC */ #include int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 5636816f442b59..a59b96733ea41f 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -1,4 +1,5 @@ -/* SPDX-License-Identifier: GPL-2.0 */ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright 2025 Google LLC */ #include #include From af9d76f5a9647328189740d3277926fe6e3cb987 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 12:03:44 +0000 Subject: [PATCH 19/41] kfuzztest: fix parsing logic that didn't account for padding --- lib/kfuzztest/parse.c | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 0aa4150ba52dc5..efa8da40b18887 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -6,12 +6,10 @@ int __kfuzztest_write_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) { + if (len != arg_size) return -EINVAL; - } - if (simple_write_to_buffer((void *)arg, arg_size, off, buf, len) < 0) { + if (simple_write_to_buffer((void *)arg, arg_size, off, buf, len) < 0) return -EFAULT; - } return 0; } @@ -20,8 +18,6 @@ int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_table **ret_reloc_table, void **ret_payload_start, void **ret_payload_end) { - pr_info("[ENTER] %s", __FUNCTION__); - int err; void *payload_end, *payload_start; size_t reloc_table_size, regions_size; struct reloc_table *rt; @@ -37,39 +33,23 @@ int __kfuzztest_parse_input(void *input, size_t input_size, regions_size = sizeof(*regions) + regions->num_regions * sizeof(struct reloc_region); - pr_info("kfuzztest: num regions = %u", regions->num_regions); - rt = (struct reloc_table *)((char *)regions + regions_size); - if ((char *)rt > (char *)payload_end) { - err = -EINVAL; - goto fail; - } + if ((char *)rt > (char *)payload_end) + return -EINVAL; reloc_table_size = sizeof(*rt) + rt->num_entries * sizeof(struct reloc_entry) + rt->payload_offset; - if ((char *)rt + reloc_table_size > (char *)payload_end) { - err = -EINVAL; - goto fail; - } - - pr_info("kfuzztest: num relocations = %u, size = %zu", rt->num_entries, - reloc_table_size); - - payload_start = (char *)(rt->entries + rt->num_entries); - if ((char *)payload_start > (char *)payload_end) { - err = -EINVAL; - goto fail; - } + if ((char *)rt + reloc_table_size > (char *)payload_end) + return -EINVAL; - pr_info("kfuzztest: payload: [ %px, %px )", payload_start, payload_end); + payload_start = (char *)(rt) + reloc_table_size; + if ((char *)payload_start > (char *)payload_end) + return -EINVAL; *ret_regions = regions; *ret_reloc_table = rt; *ret_payload_start = payload_start; *ret_payload_end = payload_end; return 0; -fail: - kfree(input); - return err; } From 1a286cb8f0186ff4d1facb523c29123cd07eb915 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 12:04:17 +0000 Subject: [PATCH 20/41] kfuzztest: update examples file --- lib/kfuzztest/kfuzztest_examples.c | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index 58971edd4965c0..d7d1fae6e54665 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -15,33 +15,17 @@ struct nested_buffers { * * | a | b | pad[8] | *a | pad[8] | *b | * - * In DISTINCT mode, this will result in 3 distinct kmalloc'd regions, and - * in POISONED mode, the buffer is untouched but the padding will be poisoned. + * We expect to see a KASAN warning by overflowing one byte into the A buffer. * - * In this test case, we look to see that a KASAN warning is triggered in both - * cases when overflowing on *a by one. */ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) { - u32 a_size; - u32 b_size; - KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, a); KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, b); KFUZZTEST_ANNOTATE_LEN(nested_buffers, a_len, a); KFUZZTEST_ANNOTATE_LEN(nested_buffers, b_len, b); - volatile char c; - pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); - pr_info("b = [%px, %px)", arg->b, arg->b + arg->b_len); - pr_info("a_len = %zu", arg->a_len); - pr_info("b_len = %zu", arg->b_len); - - a_size = KFUZZTEST_REGION_SIZE(1); - b_size = KFUZZTEST_REGION_SIZE(2); - pr_info("actual sizes = %u, %u", a_size, b_size); - /* Buffer overflow out of a bounds. This should be caught by KASAN. */ - for (size_t i = 0; i <= a_size; i++) - c = arg->a[i]; + for (size_t i = 0; i <= arg->a_len; i++) + READ_ONCE(arg->a[i]); } From b25969f22ee711668639f502259b32c46fc0a458 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 12:04:55 +0000 Subject: [PATCH 21/41] kfuzztest: update state name from st to state --- lib/kfuzztest/kfuzztest_main.c | 62 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index 766452b63ec449..a0490b1088e69a 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -55,7 +55,7 @@ struct kfuzztest_state { }; /* Global static variable to hold all state for the module. */ -static struct kfuzztest_state st; +static struct kfuzztest_state state; const umode_t KFUZZTEST_INPUT_PERMS = 0222; @@ -79,55 +79,57 @@ static int __init kfuzztest_init(void) num_test_cases = __kfuzztest_targets_end - __kfuzztest_targets_start; - st.debugfs_state = + state.debugfs_state = kmalloc(num_test_cases * sizeof(struct kfuzztest_debugfs_state), GFP_KERNEL); - if (!st.debugfs_state) + if (!state.debugfs_state) return -ENOMEM; - else if (IS_ERR(st.debugfs_state)) - return PTR_ERR(st.debugfs_state); + else if (IS_ERR(state.debugfs_state)) + return PTR_ERR(state.debugfs_state); /* Create the main "kfuzztest" directory in /sys/kernel/debug. */ - st.kfuzztest_dir = debugfs_create_dir("kfuzztest", NULL); - if (!st.kfuzztest_dir) { + state.kfuzztest_dir = debugfs_create_dir("kfuzztest", NULL); + if (!state.kfuzztest_dir) { pr_warn("KFuzzTest: could not create debugfs"); return -ENODEV; } - if (IS_ERR(st.kfuzztest_dir)) { - st.kfuzztest_dir = NULL; - return PTR_ERR(st.kfuzztest_dir); + if (IS_ERR(state.kfuzztest_dir)) { + state.kfuzztest_dir = NULL; + return PTR_ERR(state.kfuzztest_dir); } for (targ = __kfuzztest_targets_start; targ < __kfuzztest_targets_end; targ++, i++) { /* Create debugfs directory for the target. */ - st.debugfs_state[i].target_dir = - debugfs_create_dir(targ->name, st.kfuzztest_dir); + state.debugfs_state[i].target_dir = + debugfs_create_dir(targ->name, state.kfuzztest_dir); - if (!st.debugfs_state[i].target_dir) { + if (!state.debugfs_state[i].target_dir) { ret = -ENOMEM; goto cleanup_failure; - } else if (IS_ERR(st.debugfs_state[i].target_dir)) { - ret = PTR_ERR(st.debugfs_state[i].target_dir); + } else if (IS_ERR(state.debugfs_state[i].target_dir)) { + ret = PTR_ERR(state.debugfs_state[i].target_dir); goto cleanup_failure; } /* Create an input file under the target's directory. */ - st.debugfs_state[i].input_dentry.fops = + state.debugfs_state[i].input_dentry.fops = (struct file_operations){ .owner = THIS_MODULE, .write = targ->write_input_cb, }; - st.debugfs_state[i].input_dentry.dentry = debugfs_create_file( - "input", KFUZZTEST_INPUT_PERMS, - st.debugfs_state[i].target_dir, NULL, - &st.debugfs_state[i].input_dentry.fops); - if (!st.debugfs_state[i].input_dentry.dentry) { + state.debugfs_state[i].input_dentry.dentry = + debugfs_create_file( + "input", KFUZZTEST_INPUT_PERMS, + state.debugfs_state[i].target_dir, NULL, + &state.debugfs_state[i].input_dentry.fops); + if (!state.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); + } else if (IS_ERR(state.debugfs_state[i].input_dentry.dentry)) { + ret = PTR_ERR( + state.debugfs_state[i].input_dentry.dentry); goto cleanup_failure; } @@ -137,22 +139,22 @@ static int __init kfuzztest_init(void) return 0; cleanup_failure: - debugfs_remove_recursive(st.kfuzztest_dir); + debugfs_remove_recursive(state.kfuzztest_dir); return ret; } static void __exit kfuzztest_exit(void) { pr_info("KFuzzTest: exiting"); - if (!st.kfuzztest_dir) + if (!state.kfuzztest_dir) return; - debugfs_remove_recursive(st.kfuzztest_dir); - st.kfuzztest_dir = NULL; + debugfs_remove_recursive(state.kfuzztest_dir); + state.kfuzztest_dir = NULL; - if (st.debugfs_state) { - kfree(st.debugfs_state); - st.debugfs_state = NULL; + if (state.debugfs_state) { + kfree(state.debugfs_state); + state.debugfs_state = NULL; } } From 3f96f02b00b7de0713c0921ed894f493964b5213 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 12:05:17 +0000 Subject: [PATCH 22/41] kfuzztest: remove extraneous braces, prints, and add a check in poison Checks that KASAN is enabled before poisoning a region. --- lib/kfuzztest/relocations.c | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index a59b96733ea41f..6021bc5be579a4 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -22,33 +22,26 @@ static void kfuzztest_poison_range(void *start, void *end) uintptr_t head_granule_start; size_t head_prefix_size; + if (!IS_ENABLED(CONFIG_KASAN)) + return; + if (start_addr >= end_addr) return; head_granule_start = ALIGN_DOWN(start_addr, 0x8); head_prefix_size = start_addr - head_granule_start; - if (IS_ENABLED(CONFIG_KASAN_GENERIC) && head_prefix_size > 0) { + if (IS_ENABLED(CONFIG_KASAN_GENERIC) && head_prefix_size > 0) kasan_poison_last_granule((void *)head_granule_start, head_prefix_size); - pr_info("kfuzztest: poisoned [%px, %px)", - (void *)head_granule_start + head_prefix_size, - (void *)head_granule_start + 8); - } poison_body_start = ALIGN(start_addr, 0x8); poison_body_end = ALIGN_DOWN(end_addr, 0x8); - pr_info("kfuzztest: want to additionally poison [%px, %px)", - (void *)poison_body_start, (void *)poison_body_end); - if (poison_body_start < poison_body_end) { + if (poison_body_start < poison_body_end) kasan_poison((void *)poison_body_start, poison_body_end - poison_body_start, POISON_REGION_END, false); - - pr_info("kfuzztest: poisoned [%px, %px)", - (void *)poison_body_start, (void *)poison_body_end); - } } int __kfuzztest_relocate(struct reloc_region_array *regions, From 1cba6b1e83c4957747b6fa9d0e3a2af90c206e07 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 13:10:44 +0000 Subject: [PATCH 23/41] kfuzztest: add a debug helper function --- include/linux/kfuzztest.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 4ec7813cfbea33..0e2f41abcfeea1 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -151,6 +151,37 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end); +/* + * Dump some information on the parsed headers and payload. Can be useful for + * debugging inputs when writing an encoder for the KFuzzTest input format. + */ +__attribute__((unused)) static void +__kfuzztest_debug_header(struct reloc_region_array *regions, + struct reloc_table *rt, void *payload_start, + void *payload_end) +{ + uint32_t i; + pr_info("regions: { num_regions = %u } @ %px", regions->num_regions, + regions); + for (i = 0; i < regions->num_regions; i++) { + pr_info(" region_%u: { start: 0x%x, size: 0x%x }", i, + regions->regions[i].start, regions->regions[i].size); + } + + pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", + rt->num_entries, rt->payload_offset, + (char *)rt - (char *)regions); + for (i = 0; i < rt->num_entries; i++) { + pr_info(" reloc_%u: { src: %u, offset: 0x%x, dst: %u }", i, + rt->entries[i].region_id, rt->entries[i].region_offset, + rt->entries[i].value); + } + + pr_info("payload: [0x%lx, 0x%lx)", + (char *)payload_start - (char *)regions, + (char *)payload_end - (char *)regions); +} + struct kfuzztest_target { const char *name; const char *arg_type_name; From c85f16b02529ba72c67097dcddde9163bc6368ed Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 13:12:10 +0000 Subject: [PATCH 24/41] kfuzztest: poison region preceding payload to catch underflows --- lib/kfuzztest/relocations.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 6021bc5be579a4..6c515afd3692e9 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -98,5 +98,12 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, kfuzztest_poison_range(poison_start, poison_end); } + + /* + * Poison the area preceding the payload. This corresponds to the end + * of the relocation table, which we can safely discard as it is no + * longer needed. + */ + kfuzztest_poison_range((char *)payload_start - 8, payload_start); return 0; } From 4820368849488faef4963e3c1f9f2edf33fa4796 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 13:11:17 +0000 Subject: [PATCH 25/41] kfuzztest: update overflow example, add underflow example --- lib/kfuzztest/kfuzztest_examples.c | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index d7d1fae6e54665..4316f7a2a64939 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -20,12 +20,48 @@ struct nested_buffers { */ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) { + size_t i; + KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, a); KFUZZTEST_EXPECT_NOT_NULL(nested_buffers, b); KFUZZTEST_ANNOTATE_LEN(nested_buffers, a_len, a); KFUZZTEST_ANNOTATE_LEN(nested_buffers, b_len, b); - /* Buffer overflow out of a bounds. This should be caught by KASAN. */ - for (size_t i = 0; i <= arg->a_len; i++) + pr_info("a = [%px, %px)", arg->a, arg->a + KFUZZTEST_REGION_SIZE(1)); + pr_info("b = [%px, %px)", arg->b, arg->b + KFUZZTEST_REGION_SIZE(2)); + + /* Ensure that all bytes in arg->b are accessible. */ + for (i = 0; i < arg->b_len; i++) + READ_ONCE(arg->b[i]); + /* + * Check that all bytes in arg->a are accessible, and provoke an OOB + * on the first byte to the right of the buffer which will trigger + * a KASAN report. + */ + for (i = 0; i < arg->a_len; i++) READ_ONCE(arg->a[i]); } + +struct some_buffer { + char *buf; + size_t buflen; +}; + +FUZZ_TEST(test_underflow_on_buffer, struct some_buffer) +{ + size_t i; + + KFUZZTEST_EXPECT_NOT_NULL(some_buffer, buf); + KFUZZTEST_ANNOTATE_LEN(some_buffer, buflen, buf); + + pr_info("buf = [%px, %px)", arg->buf, arg->buf + arg->buflen); + + /* First ensure that all bytes in arg->b are accessible. */ + for (i = 0; i < arg->buflen; i++) + READ_ONCE(arg->buf[i]); + /* + * Provoke a buffer overflow on the first byte preceding b, triggering + * a KASAN report. + */ + READ_ONCE(*((char *)arg->buf - 1)); +} From 366d8bb02b57e4bf050f36cd9c9a731a7b5f424e Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 14:42:17 +0000 Subject: [PATCH 26/41] kfuzztest: define macros for granule size and slab redzone --- lib/kfuzztest/relocations.c | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 6c515afd3692e9..94418d89231e8e 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -3,7 +3,13 @@ #include #include -#define POISON_REGION_END 0xFC +/* + * XXX: These are both defined in `mm/kasan/kasan.h`, but the build breaks if we + * define them in `include/linux/kasan.h` Since these values are unlikely to + * change, we redefine them here. + */ +#define __KASAN_SLAB_REDZONE 0xFC +#define __KASAN_GRANULE_SIZE 0x8 /** * Poison the half open interval [start, end], where end should be 8-byte @@ -15,7 +21,7 @@ */ static void kfuzztest_poison_range(void *start, void *end) { - uintptr_t end_addr = ALIGN_DOWN((uintptr_t)end, 0x8); + uintptr_t end_addr = ALIGN_DOWN((uintptr_t)end, __KASAN_GRANULE_SIZE); uintptr_t start_addr = (uintptr_t)start; uintptr_t poison_body_start; uintptr_t poison_body_end; @@ -28,20 +34,20 @@ static void kfuzztest_poison_range(void *start, void *end) if (start_addr >= end_addr) return; - head_granule_start = ALIGN_DOWN(start_addr, 0x8); + head_granule_start = ALIGN_DOWN(start_addr, __KASAN_GRANULE_SIZE); head_prefix_size = start_addr - head_granule_start; if (IS_ENABLED(CONFIG_KASAN_GENERIC) && head_prefix_size > 0) kasan_poison_last_granule((void *)head_granule_start, head_prefix_size); - poison_body_start = ALIGN(start_addr, 0x8); - poison_body_end = ALIGN_DOWN(end_addr, 0x8); + poison_body_start = ALIGN(start_addr, __KASAN_GRANULE_SIZE); + poison_body_end = ALIGN_DOWN(end_addr, __KASAN_GRANULE_SIZE); if (poison_body_start < poison_body_end) kasan_poison((void *)poison_body_start, poison_body_end - poison_body_start, - POISON_REGION_END, false); + __KASAN_SLAB_REDZONE, false); } int __kfuzztest_relocate(struct reloc_region_array *regions, @@ -69,15 +75,14 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, if (src.start >= src.size) return -EINVAL; - if (re.value == KFUZZTEST_REGIONID_NULL) { + if (re.value == KFUZZTEST_REGIONID_NULL) *ptr_location = (uintptr_t)NULL; - } else if (re.value < regions->num_regions) { + else if (re.value < regions->num_regions) { dst = regions->regions[re.value]; *ptr_location = (uintptr_t)((char *)payload_start + dst.start); - } else { + } else return -EINVAL; - } } /* Poison the padding between regions. */ @@ -86,12 +91,12 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, /* Points to the beginning of the inter-region padding */ poison_start = payload_start + reg.start + reg.size; - if (i < regions->num_regions - 1) { + if (i < regions->num_regions - 1) poison_end = payload_start + regions->regions[i + 1].start; - } else { - poison_end = poison_start + 8; - } + else + /* The last region is padded with 8 bytes. */ + poison_end = poison_start + 0x8; if ((char *)poison_end > (char *)payload_end) return -EINVAL; From 1f0ea222c4d255bf0f2b614ac7b9d0a3814d6d97 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 14:57:34 +0000 Subject: [PATCH 27/41] kfuzztest: fix example so that it actually overflows --- lib/kfuzztest/kfuzztest_examples.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index 4316f7a2a64939..39dc5a6a0f64a7 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -38,7 +38,7 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) * on the first byte to the right of the buffer which will trigger * a KASAN report. */ - for (i = 0; i < arg->a_len; i++) + for (i = 0; i <= arg->a_len; i++) READ_ONCE(arg->a[i]); } From 7b6ff7346de8acea66822efad68dc638e1219ca8 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 17:16:45 +0000 Subject: [PATCH 28/41] kfuzztest: add stricter input parsing and validation, fix nits We now check for overflows a lot more carefully, and validate the input so that regions don't point out of bounds. Fix some nits etc... --- include/linux/kfuzztest.h | 54 +++++++++++++------- lib/kfuzztest/kfuzztest_main.c | 5 ++ lib/kfuzztest/parse.c | 92 ++++++++++++++++++++++++++++------ 3 files changed, 119 insertions(+), 32 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 0e2f41abcfeea1..7642fc6956e65e 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -3,11 +3,9 @@ #ifndef KFUZZTEST_H #define KFUZZTEST_H -#include - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("Ethan Graham "); -MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); +#include +#include +#include /** * @brief The KFuzzTest Input Serialization Format @@ -45,13 +43,13 @@ MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); * struct reloc_region - Describes a single contiguous memory region in the * payload. * - * @start: The byte offset of this region from the start of the payload, which + * @offset: The byte offset of this region from the start of the payload, which * should be aligned to the alignment requirements of the region's * underlying type. * @size: The size of this region in bytes. */ struct reloc_region { - uint32_t start; + uint32_t offset; uint32_t size; }; @@ -95,6 +93,7 @@ struct reloc_table { struct reloc_entry entries[]; }; +/* Helper for copying input data from userspace. */ int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, size_t len, loff_t *off, void *arg, size_t arg_size); @@ -155,7 +154,7 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, * Dump some information on the parsed headers and payload. Can be useful for * debugging inputs when writing an encoder for the KFuzzTest input format. */ -__attribute__((unused)) static void +__attribute__((unused)) static inline void __kfuzztest_debug_header(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) @@ -165,7 +164,7 @@ __kfuzztest_debug_header(struct reloc_region_array *regions, regions); for (i = 0; i < regions->num_regions; i++) { pr_info(" region_%u: { start: 0x%x, size: 0x%x }", i, - regions->regions[i].start, regions->regions[i].size); + regions->regions[i].offset, regions->regions[i].size); } pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", @@ -188,6 +187,11 @@ struct kfuzztest_target { ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, size_t len, loff_t *off); } __attribute__((aligned(32))); + +/* + * Enforce a fixed struct size to ensure a consistent stride when iterating + * over the array of these structs in the dedicated ELF section. + */ static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target should have size 32"); @@ -269,17 +273,15 @@ static_assert(sizeof(struct kfuzztest_target) == 32, const char __user *buf, \ size_t len, loff_t *off) \ { \ - int ret; \ + void *payload_start, *payload_end, *buffer; \ struct reloc_region_array *regions; \ struct reloc_table *rt; \ - void *payload_start, *payload_end, *buffer; \ test_arg_type *arg; \ + int ret; \ \ buffer = kmalloc(len, GFP_KERNEL); \ if (!buffer) \ return -ENOMEM; \ - else if (IS_ERR(buffer)) \ - return PTR_ERR(buffer); \ ret = __kfuzztest_write_cb_common(filp, buf, len, off, buffer, \ len); \ if (ret) \ @@ -288,6 +290,8 @@ static_assert(sizeof(struct kfuzztest_target) == 32, &payload_start, &payload_end); \ if (ret) \ goto out; \ + __kfuzztest_debug_header(regions, rt, payload_start, \ + payload_end); \ ret = __kfuzztest_relocate(regions, rt, payload_start, \ payload_end); \ if (ret) \ @@ -303,7 +307,7 @@ out: \ static void kfuzztest_logic_##test_name( \ test_arg_type *arg, struct reloc_region_array *regions) -enum kfuzztest_constraint_type : uint8_t { +enum kfuzztest_constraint_type { EXPECT_EQ = 0, EXPECT_NE, EXPECT_LE, @@ -346,6 +350,10 @@ struct kfuzztest_constraint { enum kfuzztest_constraint_type type; } __attribute__((aligned(64))); +/* + * Enforce a fixed struct size to ensure a consistent stride when iterating + * over the array of these structs in the dedicated ELF section. + */ static_assert(sizeof(struct kfuzztest_constraint) == 64, "struct kfuzztest_constraint should have size 64"); @@ -389,10 +397,6 @@ static_assert(sizeof(struct kfuzztest_constraint) == 64, __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, lower_bound, \ upper_bound, EXPECT_IN_RANGE) -#define KFUZZTEST_EXPECT_LEN(expected_len, actual_len) \ - if ((expected_len) != (actual_len)) \ - return; - /** * Annotations express attributes about structure fields that can't be easily * or safely verified at runtime. They are intended as hints to the fuzzing @@ -442,6 +446,13 @@ struct kfuzztest_annotation { enum kfuzztest_annotation_attribute attrib; } __attribute__((aligned(32))); +/* + * Enforce a fixed struct size to ensure a consistent stride when iterating + * over the array of these structs in the dedicated ELF section. + */ +static_assert(sizeof(struct kfuzztest_annotation) == 32, + "struct kfuzztest_annotation should have size 32"); + #define __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, attribute) \ static struct kfuzztest_annotation __annotation_##arg_type##_##field \ __attribute__((__section__(".kfuzztest_annotation"), \ @@ -478,4 +489,11 @@ struct kfuzztest_annotation { #define KFUZZTEST_REGION_SIZE(n) \ ((n) < (regions->num_regions) ? (regions->regions[n].size) : 0) +/** + * The end of the input should be padded by at least this number of bytes as + * it is poisoned to detect out of bounds accesses at the end of the last + * region. + */ +#define KFUZZTEST_TAIL_POISON_SIZE (0x8) + #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index a0490b1088e69a..3cf4ef22dd577b 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -11,8 +11,13 @@ #include #include #include +#include #include +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Ethan Graham "); +MODULE_DESCRIPTION("Kernel Fuzz Testing Framework (KFuzzTest)"); + extern const struct kfuzztest_target __kfuzztest_targets_start[]; extern const struct kfuzztest_target __kfuzztest_targets_end[]; diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index efa8da40b18887..b5bbf5a40fd094 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -13,38 +13,102 @@ int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, return 0; } +static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, + struct reloc_table *rt, + void *payload_start, void *payload_end) +{ + size_t payload_size = (char *)payload_end - (char *)payload_start; + size_t usable_payload_size; + uint32_t region_end_offset; + struct reloc_entry reloc; + struct reloc_region reg; + uint32_t i; + + if ((char *)payload_start > (char *)payload_end) + return false; + if (payload_size < KFUZZTEST_TAIL_POISON_SIZE) + return false; + usable_payload_size = payload_size - KFUZZTEST_TAIL_POISON_SIZE; + + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + if (check_add_overflow(reg.offset, reg.size, + ®ion_end_offset)) + return false; + if ((size_t)region_end_offset > usable_payload_size) + return false; + } + + for (i = 0; i < rt->num_entries; i++) { + reloc = rt->entries[i]; + if (reloc.region_id >= regions->num_regions) + return false; + if (reloc.value != KFUZZTEST_REGIONID_NULL && + reloc.value >= regions->num_regions) + return false; + + reg = regions->regions[reloc.region_id]; + if (reloc.region_offset % (sizeof(uintptr_t)) || + reloc.region_offset + sizeof(uintptr_t) > reg.size) + return false; + } + + return true; +} + int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_region_array **ret_regions, struct reloc_table **ret_reloc_table, void **ret_payload_start, void **ret_payload_end) { - void *payload_end, *payload_start; + size_t reloc_entries_size, reloc_regions_size; size_t reloc_table_size, regions_size; - struct reloc_table *rt; struct reloc_region_array *regions; + void *payload_end, *payload_start; + struct reloc_table *rt; + size_t curr_offset = 0; if (input_size < sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) return -EINVAL; - payload_end = (char *)input + input_size; - regions = input; - regions_size = sizeof(*regions) + - regions->num_regions * sizeof(struct reloc_region); + if (check_mul_overflow(regions->num_regions, + sizeof(struct reloc_region), + &reloc_regions_size)) + return -EINVAL; + if (check_add_overflow(sizeof(*regions), reloc_regions_size, + ®ions_size)) + return -EINVAL; - rt = (struct reloc_table *)((char *)regions + regions_size); - if ((char *)rt > (char *)payload_end) + curr_offset = regions_size; + if (curr_offset > input_size) + return -EINVAL; + if (input_size - curr_offset < sizeof(struct reloc_table)) return -EINVAL; - reloc_table_size = sizeof(*rt) + - rt->num_entries * sizeof(struct reloc_entry) + - rt->payload_offset; - if ((char *)rt + reloc_table_size > (char *)payload_end) + rt = (struct reloc_table *)((char *)input + curr_offset); + + if (check_mul_overflow((size_t)rt->num_entries, + sizeof(struct reloc_entry), &reloc_entries_size)) + return -EINVAL; + if (check_add_overflow(sizeof(*rt), reloc_entries_size, + &reloc_table_size)) + return -EINVAL; + if (check_add_overflow(reloc_table_size, rt->payload_offset, + &reloc_table_size)) return -EINVAL; - payload_start = (char *)(rt) + reloc_table_size; - if ((char *)payload_start > (char *)payload_end) + if (check_add_overflow(curr_offset, reloc_table_size, &curr_offset)) + return -EINVAL; + if (curr_offset > input_size) + return -EINVAL; + + payload_start = (char *)input + curr_offset; + payload_end = (char *)input + input_size; + + if (!__kfuzztest_input_is_valid(regions, rt, payload_start, + payload_end)) return -EINVAL; *ret_regions = regions; From f1cd01f52e370be4052d4ba7bbd52ebb74164908 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 17:51:11 +0000 Subject: [PATCH 29/41] kfuzztest: more validation. --- include/linux/kfuzztest.h | 25 ++++++++++++++++++------- lib/kfuzztest/parse.c | 19 ++++++++++++++++--- lib/kfuzztest/relocations.c | 34 ++++++++-------------------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 7642fc6956e65e..cd169dabe54913 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -17,23 +17,23 @@ * further kernel allocations. Pointers are patched internally using a "region" * system where each region corresponds to some pointed-to data. * - * Regions should be padded to respect alignment constraints of their - * underlying types. These padded regions are poisoned by KFuzzTest to ensure - * that KASAN catches OOB accesses. + * Regions should be padded to respect alignment constraints of their underlying + * types, and should be followed by at least 8 bytes of padding. These padded + * regions are poisoned by KFuzzTest to ensure that KASAN catches OOB accesses. * * The format consists of three main components: * 1. A reloc_region_array: Defines the memory layout of the target structure * by partitioning the payload into logical regions. Each logical region * should contain the byte representation of the type that it represents, - * including any necessary padding. + * including any necessary padding. The region descriptors should be + * ordered by offset ascending. * 2. A reloc_table: Provides "linking" instructions that tell the kernel how * to patch pointer fields to point to the correct regions. By design, * the first region (index 0) is passed as input into a FUZZ_TEST. * 3. A Payload: The raw binary data for the structure and its associated * buffers. This should be aligned to the maximum alignment of all * regions to satisfy alignment requirements of the input types, but this - * isn't checked by the parser. The payload should have at least 8 bytes - * of trailing padding. + * isn't checked by the parser. * * For a detailed specification of the binary layout see the full documentation * at: Documentation/dev-tools/kfuzztest.rst @@ -145,6 +145,9 @@ int __kfuzztest_parse_input(void *input, size_t input_size, * * Returns: 0 on success, or a negative error code if the relocation data is * found to be corrupt (e.g., invalid pointers). + * + * NOTE: this function only performs basic input validation. Full input + * validation is handled during parsing by __kfuzztest_parse_input. */ int __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, @@ -489,11 +492,19 @@ static_assert(sizeof(struct kfuzztest_annotation) == 32, #define KFUZZTEST_REGION_SIZE(n) \ ((n) < (regions->num_regions) ? (regions->regions[n].size) : 0) +/* + * FIXME: These are both defined in `mm/kasan/kasan.h`, but the build breaks + * if we define them in `include/linux/kasan.h` Since these values are unlikely + * to change, we redefine them here. + */ +#define __KASAN_SLAB_REDZONE 0xFC +#define __KASAN_GRANULE_SIZE 0x8 + /** * The end of the input should be padded by at least this number of bytes as * it is poisoned to detect out of bounds accesses at the end of the last * region. */ -#define KFUZZTEST_TAIL_POISON_SIZE (0x8) +#define KFUZZTEST_POISON_SIZE __KASAN_GRANULE_SIZE #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index b5bbf5a40fd094..4f3df5b1319cd0 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -18,17 +18,17 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, void *payload_start, void *payload_end) { size_t payload_size = (char *)payload_end - (char *)payload_start; + struct reloc_region reg, next_reg; size_t usable_payload_size; uint32_t region_end_offset; struct reloc_entry reloc; - struct reloc_region reg; uint32_t i; if ((char *)payload_start > (char *)payload_end) return false; - if (payload_size < KFUZZTEST_TAIL_POISON_SIZE) + if (payload_size < KFUZZTEST_POISON_SIZE) return false; - usable_payload_size = payload_size - KFUZZTEST_TAIL_POISON_SIZE; + usable_payload_size = payload_size - KFUZZTEST_POISON_SIZE; for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; @@ -37,6 +37,19 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, return false; if ((size_t)region_end_offset > usable_payload_size) return false; + + if (i < regions->num_regions - 1) { + next_reg = regions->regions[i + 1]; + if (reg.offset > next_reg.offset) + return false; + /* + * Enforce the minimum poisonable gap between + * consecutive regions. + */ + if (reg.offset + reg.size + KFUZZTEST_POISON_SIZE > + next_reg.offset) + return false; + } } for (i = 0; i < rt->num_entries; i++) { diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 94418d89231e8e..fa4ae848e1f9f6 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -3,14 +3,6 @@ #include #include -/* - * XXX: These are both defined in `mm/kasan/kasan.h`, but the build breaks if we - * define them in `include/linux/kasan.h` Since these values are unlikely to - * change, we redefine them here. - */ -#define __KASAN_SLAB_REDZONE 0xFC -#define __KASAN_GRANULE_SIZE 0x8 - /** * Poison the half open interval [start, end], where end should be 8-byte * aligned if it is not, then we cannot guarantee that the whole range will @@ -23,9 +15,9 @@ static void kfuzztest_poison_range(void *start, void *end) { uintptr_t end_addr = ALIGN_DOWN((uintptr_t)end, __KASAN_GRANULE_SIZE); uintptr_t start_addr = (uintptr_t)start; + uintptr_t head_granule_start; uintptr_t poison_body_start; uintptr_t poison_body_end; - uintptr_t head_granule_start; size_t head_prefix_size; if (!IS_ENABLED(CONFIG_KASAN)) @@ -54,33 +46,23 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) { - size_t i; struct reloc_region reg, src, dst; + void *poison_start, *poison_end; uintptr_t *ptr_location; struct reloc_entry re; - void *poison_start, *poison_end; + size_t i; /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; - - if (re.region_id >= regions->num_regions) - return -EINVAL; - src = regions->regions[re.region_id]; - - ptr_location = (uintptr_t *)((char *)payload_start + src.start + - re.region_offset); - if ((char *)ptr_location >= (char *)payload_end) - return -EINVAL; - if (src.start >= src.size) - return -EINVAL; - + ptr_location = (uintptr_t *)((char *)payload_start + + src.offset + re.region_offset); if (re.value == KFUZZTEST_REGIONID_NULL) *ptr_location = (uintptr_t)NULL; else if (re.value < regions->num_regions) { dst = regions->regions[re.value]; *ptr_location = - (uintptr_t)((char *)payload_start + dst.start); + (uintptr_t)((char *)payload_start + dst.offset); } else return -EINVAL; } @@ -90,10 +72,10 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, reg = regions->regions[i]; /* Points to the beginning of the inter-region padding */ - poison_start = payload_start + reg.start + reg.size; + poison_start = payload_start + reg.offset + reg.size; if (i < regions->num_regions - 1) poison_end = - payload_start + regions->regions[i + 1].start; + payload_start + regions->regions[i + 1].offset; else /* The last region is padded with 8 bytes. */ poison_end = poison_start + 0x8; From be3ba1896028a54ec9a8c0b4f54f467f6ea9db56 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 18:41:11 +0000 Subject: [PATCH 30/41] kfuzztest: assume at 8 bytes of padding before payload We now assume that there is at least 8 bytes of padding before the payload. This means that we never poison the relocation table, which isn't a bad thing in my opinion. --- include/linux/kfuzztest.h | 11 +++++------ lib/kfuzztest/parse.c | 8 +++++++- lib/kfuzztest/relocations.c | 10 ++++------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index cd169dabe54913..d52ff6ece02a53 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -83,13 +83,14 @@ struct reloc_entry { * input. * * @num_entries: the number of pointer relocations. - * @payload_offset: the number of padded bytes between the last relocation in - * entries, and the start of the payload data. + * @padding_size: the number of padded bytes between the last relocation in + * entries, and the start of the payload data. This should be at least + * 8 bytes, as it is used for poisoning. * @entries: array of relocations. */ struct reloc_table { uint32_t num_entries; - uint32_t payload_offset; + uint32_t padding_size; struct reloc_entry entries[]; }; @@ -171,7 +172,7 @@ __kfuzztest_debug_header(struct reloc_region_array *regions, } pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", - rt->num_entries, rt->payload_offset, + rt->num_entries, rt->padding_size, (char *)rt - (char *)regions); for (i = 0; i < rt->num_entries; i++) { pr_info(" reloc_%u: { src: %u, offset: 0x%x, dst: %u }", i, @@ -293,8 +294,6 @@ static_assert(sizeof(struct kfuzztest_target) == 32, &payload_start, &payload_end); \ if (ret) \ goto out; \ - __kfuzztest_debug_header(regions, rt, payload_start, \ - payload_end); \ ret = __kfuzztest_relocate(regions, rt, payload_start, \ payload_end); \ if (ret) \ diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 4f3df5b1319cd0..e23d8ae88d151a 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -52,6 +52,12 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, } } + if (rt->padding_size < KFUZZTEST_POISON_SIZE) { + pr_info("validation failed because rt->padding_size = %u", + rt->padding_size); + return false; + } + for (i = 0; i < rt->num_entries; i++) { reloc = rt->entries[i]; if (reloc.region_id >= regions->num_regions) @@ -108,7 +114,7 @@ int __kfuzztest_parse_input(void *input, size_t input_size, if (check_add_overflow(sizeof(*rt), reloc_entries_size, &reloc_table_size)) return -EINVAL; - if (check_add_overflow(reloc_table_size, rt->payload_offset, + if (check_add_overflow(reloc_table_size, rt->padding_size, &reloc_table_size)) return -EINVAL; diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index fa4ae848e1f9f6..60434509788046 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -55,6 +55,7 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, /* Patch pointers. */ for (i = 0; i < rt->num_entries; i++) { re = rt->entries[i]; + src = regions->regions[re.region_id]; ptr_location = (uintptr_t *)((char *)payload_start + src.offset + re.region_offset); if (re.value == KFUZZTEST_REGIONID_NULL) @@ -86,11 +87,8 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, kfuzztest_poison_range(poison_start, poison_end); } - /* - * Poison the area preceding the payload. This corresponds to the end - * of the relocation table, which we can safely discard as it is no - * longer needed. - */ - kfuzztest_poison_range((char *)payload_start - 8, payload_start); + /* Poison the padded area preceding the payload. */ + kfuzztest_poison_range((char *)payload_start - rt->padding_size, + payload_start); return 0; } From eab275f3cc52adc2b69515e73c61788188ae6fb5 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Thu, 7 Aug 2025 18:46:11 +0000 Subject: [PATCH 31/41] kfuzztest: add GE and LT domain constraints for completeness --- include/linux/kfuzztest.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index d52ff6ece02a53..577ed315712d10 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -312,8 +312,10 @@ out: \ enum kfuzztest_constraint_type { EXPECT_EQ = 0, EXPECT_NE, + EXPECT_LT, EXPECT_LE, EXPECT_GT, + EXPECT_GE, EXPECT_IN_RANGE, }; @@ -380,6 +382,11 @@ static_assert(sizeof(struct kfuzztest_constraint) == 64, return; \ __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_NE) +#define KFUZZTEST_EXPECT_LT(arg_type, field, val) \ + if (arg->field >= val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_LT) + #define KFUZZTEST_EXPECT_LE(arg_type, field, val) \ if (arg->field > val) \ return; \ @@ -390,6 +397,11 @@ static_assert(sizeof(struct kfuzztest_constraint) == 64, return; \ __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GT) +#define KFUZZTEST_EXPECT_GE(arg_type, field, val) \ + if (arg->field < val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GE) + #define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) \ KFUZZTEST_EXPECT_NE(arg_type, field, 0x0) From c223a376a322d4da47405c9705b627cf43558ac7 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:14:14 +0000 Subject: [PATCH 32/41] kfuzztest: remove __kfuzztest_write_cb_common as we no longer need it. --- include/linux/kfuzztest.h | 10 ++-------- lib/kfuzztest/parse.c | 11 ----------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 577ed315712d10..8cb796c31b4315 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -94,11 +94,6 @@ struct reloc_table { struct reloc_entry entries[]; }; -/* Helper for copying input data from userspace. */ -int __kfuzztest_write_cb_common(struct file *filp, const char __user *buf, - size_t len, loff_t *off, void *arg, - size_t arg_size); - /** * __kfuzztest_parse_input validates and parses the KFuzzTest input format. * @@ -286,9 +281,8 @@ static_assert(sizeof(struct kfuzztest_target) == 32, buffer = kmalloc(len, GFP_KERNEL); \ if (!buffer) \ return -ENOMEM; \ - ret = __kfuzztest_write_cb_common(filp, buf, len, off, buffer, \ - len); \ - if (ret) \ + ret = simple_write_to_buffer(buffer, len, off, buf, len); \ + if (ret < 0) \ goto out; \ ret = __kfuzztest_parse_input(buffer, len, ®ions, &rt, \ &payload_start, &payload_end); \ diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index e23d8ae88d151a..1e0967d7bac23f 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -2,17 +2,6 @@ /* Copyright 2025 Google LLC */ #include -int __kfuzztest_write_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((void *)arg, arg_size, off, buf, len) < 0) - return -EFAULT; - return 0; -} - static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) From 55f9fe629fe3a63130e3c4db9fca765be45294bf Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:20:40 +0000 Subject: [PATCH 33/41] kfuzztest: documentation updates --- Documentation/dev-tools/kfuzztest.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst index 1c8723a5b9b980..5a87f7af14c07e 100644 --- a/Documentation/dev-tools/kfuzztest.rst +++ b/Documentation/dev-tools/kfuzztest.rst @@ -72,7 +72,7 @@ The following example illustrates how to create a fuzz target for a function // 3. (Optional) Add constraints to define preconditions. // This check ensures 'arg->data' is not NULL. If the condition // is not met, the test exits early. This also creates metadata - // to inform the fuzzer + // to inform the fuzzer. KFUZZTEST_EXPECT_NOT_NULL(process_data_inputs, data); // 4. (Optional) Add annotations to provide semantic hints. @@ -82,8 +82,8 @@ The following example illustrates how to create a fuzz target for a function KFUZZTEST_ANNOTATE_LEN(process_data_inputs, len, data); // 5. Call the kernel function with the provided inputs. - // Memory errors like out-of-bounds accesses on 'arg->data' - // will be detected by KASAN. + // Memory errors like out-of-bounds accesses on 'arg->data' will + // be detected by KASAN or other memory error detection tools. process_data(arg->data, arg->len); } @@ -115,9 +115,9 @@ single kernel allocation and copy from userspace. The format consists of three main parts laid out sequentially: a region array, a relocation table, and the payload.:: - +----------------+---------------------+-----------------+----------------+ - | region array | relocation table | padding | payload | - +----------------+---------------------+-----------------+----------------+ + +----------------+---------------------+-----------+----------------+ + | region array | relocation table | padding | payload | + +----------------+---------------------+-----------+----------------+ Region Array ~~~~~~~~~~~~ @@ -149,8 +149,9 @@ Relocation Table ~~~~~~~~~~~~~~~~ The relocation table provides the instructions for the kernel to "hydrate" the -payload by patching pointer fields. It contains an array of struct reloc_entry -items. Each entry acts as a linking instruction, specifying: +payload by patching pointer fields. It contains an array of +``struct reloc_entry`` items. Each entry acts as a linking instruction, +specifying: - The location of a pointer that needs to be patched (identified by a region ID and an offset within that region). @@ -173,7 +174,8 @@ according to their specified offsets. its own type's alignment. - Padding and Poisoning: The space between the end of one region's data and the - beginning of the next must be sufficient for padding. KFuzzTest poisons this - unused padding with KASAN, allowing for precise detection of out-of-bounds - memory accesses between adjacent buffers. This padding should be at least - ``KFUZZTEST_POISON_SIZE`` bytes as defined in `include/linux/kfuzztest.h``. + beginning of the next must be sufficient for padding. In KASAN builds, + KFuzzTest poisons this unused padding, allowing for precise detection of + out-of-bounds memory accesses between adjacent buffers. This padding should + be at least ``KFUZZTEST_POISON_SIZE`` bytes as defined in + `include/linux/kfuzztest.h``. From 5ba009f39971e493a197f39c68f38e597e50ce8e Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:30:36 +0000 Subject: [PATCH 34/41] kfuzztest: updates doc comments for consistency --- include/linux/kfuzztest.h | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 8cb796c31b4315..4abee465e3b973 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -40,8 +40,7 @@ */ /** - * struct reloc_region - Describes a single contiguous memory region in the - * payload. + * struct reloc_region - single contiguous memory region in the payload * * @offset: The byte offset of this region from the start of the payload, which * should be aligned to the alignment requirements of the region's @@ -54,7 +53,7 @@ struct reloc_region { }; /** - * struct reloc_region_array - An array of all memory regions. + * struct reloc_region_array - array of regions in an input * @num_regions: The total number of regions defined. * @regions: A flexible array of `num_regions` region descriptors. */ @@ -64,7 +63,7 @@ struct reloc_region_array { }; /** - * struct reloc_entry describes a single pointer to be patched. + * struct reloc_entry - a single pointer to be patched in an input * * @region_id: The index of the region in the `reloc_region_array` that * contains the pointer. @@ -79,8 +78,7 @@ struct reloc_entry { }; /** - * struct reloc_entry is an array of all pointer relocations required by an - * input. + * struct reloc_entry - array of relocations required by an input * * @num_entries: the number of pointer relocations. * @padding_size: the number of padded bytes between the last relocation in @@ -95,7 +93,7 @@ struct reloc_table { }; /** - * __kfuzztest_parse_input validates and parses the KFuzzTest input format. + * __kfuzztest_parse_input - parse and validate a KFuzzTest input * * @input: A buffer containing the serialized test case. * @input_size: The size in bytes of the @input buffer. @@ -108,13 +106,7 @@ struct reloc_table { * @ret_payload_end: On success, updated to point to the first byte after the * end of the data payload. * - * This function performs the initial validation of the binary input blob. It - * checks that the declared sizes of the region array and relocation table are - * within the bounds of the total input size and extracts pointers to the - * main components of the format. It does not perform a deep validation of - * the entries themselves, which is instead done by __kfuzztest_relocate. - * - * Returns: 0 on success, or a negative error code on failure. + * Returns: 0 on success, or a negative error code if the input is corrupted. */ int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_region_array **ret_regions, @@ -122,7 +114,7 @@ int __kfuzztest_parse_input(void *input, size_t input_size, void **ret_payload_start, void **ret_payload_end); /** - * __kfuzztest_relocate hydrates the payload by patching pointers. + * __kfuzztest_relocate - resolve relocations in a serialized payload * * @regions: The relocation region array parsed from the input. * @rt: The relocation table parsed from the input. @@ -132,9 +124,9 @@ int __kfuzztest_parse_input(void *input, size_t input_size, * This function iterates through the region array and relocation table to * patch the pointers inside of the payload, reconstructing pointer-pointee * relationships between the logical regions of the fuzz driver input. For - * each entry in @rt, it calculates the address the address of a pointer field - * within the payload and sets it to the start address of its target region, or - * a NULL pointer if marked with KFUZZTEST_REGIONID_NULL. + * each entry in @rt, it calculates the address of a pointer field within the + * payload and sets it to the start address of its target region, or a NULL + * pointer if marked with KFUZZTEST_REGIONID_NULL. * * The padded areas between each region are poisoned with a KASAN slab redzone * to enable the detection of byte-accurate OOB accesses in the fuzz logic. @@ -195,7 +187,7 @@ static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target should have size 32"); /** - * FUZZ_TEST defines a KFuzzTest target. + * FUZZ_TEST - defines a KFuzzTest target * * @test_name: The unique identifier for the fuzz test, which is used to name * the debugfs entry, e.g., /sys/kernel/debug/kftf/@test_name. @@ -314,8 +306,7 @@ enum kfuzztest_constraint_type { }; /** - * struct kfuzztest_constraint defines a metadata record for a domain - * constraint. + * struct kfuzztest_constraint - a metadata record for a domain constraint * * Domain constraints are rules about the input data that must be satisfied for * a fuzz test to proceed. While they are enforced in the kernel with a runtime @@ -424,7 +415,7 @@ enum kfuzztest_annotation_attribute : uint8_t { }; /** - * struct kfuzztest_annotation defines a metadata record for a fuzzer hint. + * struct kfuzztest_annotation - a metadata record for a fuzzer hint * * This struct captures a single hint about a field in the input structure. * Instances are generated by the KFUZZTEST_ANNOTATE_* macros and are placed From c22f0fc4fcf04a3a04eb5f1b0b5fc7021a4bbdef Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:31:06 +0000 Subject: [PATCH 35/41] kfuzztest: update allocation / error handling for debufs_state --- lib/kfuzztest/kfuzztest_main.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index 3cf4ef22dd577b..4f90aeb776865a 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -85,12 +85,10 @@ static int __init kfuzztest_init(void) num_test_cases = __kfuzztest_targets_end - __kfuzztest_targets_start; state.debugfs_state = - kmalloc(num_test_cases * sizeof(struct kfuzztest_debugfs_state), + kzalloc(num_test_cases * sizeof(struct kfuzztest_debugfs_state), GFP_KERNEL); if (!state.debugfs_state) return -ENOMEM; - else if (IS_ERR(state.debugfs_state)) - return PTR_ERR(state.debugfs_state); /* Create the main "kfuzztest" directory in /sys/kernel/debug. */ state.kfuzztest_dir = debugfs_create_dir("kfuzztest", NULL); From 1eee647514cb206ff82c333f8eef5d68ab54cdcf Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:50:07 +0000 Subject: [PATCH 36/41] kfuzztest: file header comments --- include/linux/kfuzztest.h | 9 ++++++++- lib/kfuzztest/kfuzztest_examples.c | 6 +++++- lib/kfuzztest/kfuzztest_main.c | 6 +----- lib/kfuzztest/parse.c | 6 +++++- lib/kfuzztest/relocations.c | 7 +++++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 4abee465e3b973..3879acb18ccbb5 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -1,5 +1,12 @@ // SPDX-License-Identifier: GPL-2.0 -/* Copyright 2025 Google LLC */ +/* + * The Kernel Fuzz Testing Framework (KFuzzTest) API for defining fuzz targets + * for internal kernel functions. + * + * For more information please see Documentation/dev-tools/kfuzztest.rst. + * + * Copyright 2025 Google LLC + */ #ifndef KFUZZTEST_H #define KFUZZTEST_H diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index 39dc5a6a0f64a7..bdc182664116f4 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -1,5 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 -/* Copyright 2025 Google LLC */ +/* + * This file contains some KFuzzTest target examples. + * + * Copyright 2025 Google LLC + */ #include struct nested_buffers { diff --git a/lib/kfuzztest/kfuzztest_main.c b/lib/kfuzztest/kfuzztest_main.c index 4f90aeb776865a..fccda1319fb0d4 100644 --- a/lib/kfuzztest/kfuzztest_main.c +++ b/lib/kfuzztest/kfuzztest_main.c @@ -1,10 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 /* - * Kernel Fuzz Testing Framework (KFuzzTest) - 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/kfuzztest/ for userspace to interact with each test. + * KFuzzTest core module initialization and debugfs interface. * * Copyright 2025 Google LLC */ diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 1e0967d7bac23f..f19c8f1853e58a 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -1,5 +1,9 @@ /* SPDX-License-Identifier: GPL-2.0 */ -/* Copyright 2025 Google LLC */ +/* + * KFuzzTest input parsing and validation. + * + * Copyright 2025 Google LLC + */ #include static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 60434509788046..53751335393ff8 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 -/* Copyright 2025 Google LLC */ -#include +/* + * KFuzzTest input relocation and memory poisoning. + * + * Copyright 2025 Google LLC + */ #include /** From fd8b6ec922749c24eb4da7846acd82e4fd73b5bb Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 09:54:41 +0000 Subject: [PATCH 37/41] kfuzztest: update some kasan poison logic and comments Function now bails out when CONFIG_KASAN is not built in, which prevents checks. --- lib/kfuzztest/relocations.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 53751335393ff8..8ff87dacb362cf 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -5,14 +5,19 @@ * Copyright 2025 Google LLC */ #include +#include + +#ifdef CONFIG_KASAN /** - * Poison the half open interval [start, end], where end should be 8-byte - * aligned if it is not, then we cannot guarantee that the whole range will - * be poisoned. + * kfuzztest_poison_range - poison the memory range [start, end) + * + * The exact behavior is subject to alignment with KASAN's 8-byte granule size: * - * If start is not 8-byte-aligned, the remaining bytes in its 8-byte granule - * can only be poisoned if CONFIG_KASAN_GENERIC is enabled. + * - If @start is unaligned, the initial partial granule at the beginning + * of the range is only poisoned if CONFIG_KASAN_GENERIC is enabled. + * - The poisoning of the range only extends up to the last full granule + * before @end. Any remaining bytes in a final partial granule are ignored. */ static void kfuzztest_poison_range(void *start, void *end) { @@ -45,6 +50,12 @@ static void kfuzztest_poison_range(void *start, void *end) __KASAN_SLAB_REDZONE, false); } +#else /* CONFIG_KASAN */ + +static inline void kfuzztest_poison_range(void *, void *) {} + +#endif /* CONFIG_KASAN */ + int __kfuzztest_relocate(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) @@ -81,8 +92,7 @@ int __kfuzztest_relocate(struct reloc_region_array *regions, poison_end = payload_start + regions->regions[i + 1].offset; else - /* The last region is padded with 8 bytes. */ - poison_end = poison_start + 0x8; + poison_end = payload_end; if ((char *)poison_end > (char *)payload_end) return -EINVAL; From f7ee465847914c1331eda050fcd17289269fc2d4 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 10:08:13 +0000 Subject: [PATCH 38/41] kfuzztest: update test case comments --- lib/kfuzztest/kfuzztest_examples.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/kfuzztest/kfuzztest_examples.c b/lib/kfuzztest/kfuzztest_examples.c index bdc182664116f4..6c709d25818fda 100644 --- a/lib/kfuzztest/kfuzztest_examples.c +++ b/lib/kfuzztest/kfuzztest_examples.c @@ -19,8 +19,8 @@ struct nested_buffers { * * | a | b | pad[8] | *a | pad[8] | *b | * - * We expect to see a KASAN warning by overflowing one byte into the A buffer. - * + * where the padded regions are poisoned. We expect to trigger a KASAN report by + * overflowing one byte into the `a` buffer. */ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) { @@ -38,9 +38,9 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) for (i = 0; i < arg->b_len; i++) READ_ONCE(arg->b[i]); /* - * Check that all bytes in arg->a are accessible, and provoke an OOB - * on the first byte to the right of the buffer which will trigger - * a KASAN report. + * Check that all bytes in arg->a are accessible, and provoke an OOB on + * the first byte to the right of the buffer which will trigger a KASAN + * report. */ for (i = 0; i <= arg->a_len; i++) READ_ONCE(arg->a[i]); @@ -51,6 +51,10 @@ struct some_buffer { size_t buflen; }; +/** + * Tests that the region between struct some_buffer and the expanded *buf field + * is correctly poisoned by accessing the first byte before *buf. + */ FUZZ_TEST(test_underflow_on_buffer, struct some_buffer) { size_t i; From 5411510703e6694f66307044192683f328dcd938 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 10:19:54 +0000 Subject: [PATCH 39/41] kfuzztest: remove no longer needed CONFIG_KASAN check --- lib/kfuzztest/relocations.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c index 8ff87dacb362cf..ed188a25076721 100644 --- a/lib/kfuzztest/relocations.c +++ b/lib/kfuzztest/relocations.c @@ -28,9 +28,6 @@ static void kfuzztest_poison_range(void *start, void *end) uintptr_t poison_body_end; size_t head_prefix_size; - if (!IS_ENABLED(CONFIG_KASAN)) - return; - if (start_addr >= end_addr) return; From 7ed28762f1641ea6f811512ed707f2a79b0cae64 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 10:29:49 +0000 Subject: [PATCH 40/41] kfuzztest: update KConfig dependencies and help description --- lib/Kconfig.kfuzztest | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/Kconfig.kfuzztest b/lib/Kconfig.kfuzztest index dd9047459e051c..7a20f5325a3d44 100644 --- a/lib/Kconfig.kfuzztest +++ b/lib/Kconfig.kfuzztest @@ -2,11 +2,16 @@ config KFUZZTEST bool "KFuzzTest - enable support for internal fuzz targets" - depends on DEBUG_FS && KASAN - help - Enables support for the kernel fuzz testing framework (KFuzzTest), an - interface for exposing internal kernel functions to a userspace fuzzing - engine. KFuzzTest targets are exposed via a debugfs interface that - accepts serialized userspace inputs, and is designed to make it easier - to fuzz deeply nested kernel code that is hard to reach from the system - call boundary. + depends on DEBUG_FS + help + Enables support for the kernel fuzz testing framework (KFuzzTest), an + interface for exposing internal kernel functions to a userspace fuzzing + engine. KFuzzTest targets are exposed via a debugfs interface that + accepts serialized userspace inputs, and is designed to make it easier + to fuzz deeply nested kernel code that is hard to reach from the system + call boundary. Using a simple macro-based API, developers can add a new + fuzz target with minimal boilerplate code. + + It is strongly recommended to also enable CONFIG_KASAN for byte-accurate + out-of-bounds detection, as KFuzzTest was designed with this in mind. It + is also recommended to enable CONFIG_KCOV for coverage guided fuzzing. From 496ceefa5c78ea0fc2c28895c2d28804599f16f4 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 10:32:41 +0000 Subject: [PATCH 41/41] kfuzztest: move period out of a quoted region --- Documentation/dev-tools/kfuzztest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst index 5a87f7af14c07e..5e96eddbfda63e 100644 --- a/Documentation/dev-tools/kfuzztest.rst +++ b/Documentation/dev-tools/kfuzztest.rst @@ -50,7 +50,7 @@ structure, writing the test body using the ``FUZZ_TEST`` macro, and optionally adding metadata for the fuzzer. The following example illustrates how to create a fuzz target for a function -``int process_data(const char *data, size_t len).`` +``int process_data(const char *data, size_t len)``. .. code-block:: c