From f44cc6c7af552d28ace572137f59b0557ea8ec83 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 10:43:55 +0000 Subject: [PATCH 01/18] mm/kasan: expose poisoning functions for external modules Move the function declarations for kasan_poison and kasan_poison_last_granule from the internal mm/kasan/kasan.h to . This refactoring makes these KASAN primitives available to other kernel subsystems that may require fine-grained memory poisoning capabilities. This is a preparatory patch for the upcoming KFuzzTest framework which relies on these functions for detecting out-of-bounds memory accesses from within the bounds of a single kmalloc'd buffer. No functional change is introduced by this commit. Signed-off-by: Ethan Graham --- include/linux/kasan.h | 28 ++++++++++++++++++++++++++++ mm/kasan/kasan.h | 28 ---------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/include/linux/kasan.h b/include/linux/kasan.h index 890011071f2b14..508d8a2d5ad613 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -126,6 +126,34 @@ 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 */ + +/** + * 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); + void __kasan_poison_slab(struct slab *slab); static __always_inline void kasan_poison_slab(struct slab *slab) { diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h index 129178be5e6492..c4a478f2d22e42 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 @@ -525,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 8fdc2e680828152e0290a02456e1cde10b8e6ab5 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 11:45:17 +0000 Subject: [PATCH 02/18] kfuzztest: add user-facing API and data structures Add the foundational user-facing components for the KFuzzTest framework. This includes the main API header , the KConfig option to enable the feature, and the required linker script changes which introduce three new ELF sections in vmlinux. Note that KFuzzTest is intended strictly for debug builds only, and should never be enabled in a production build. The fact that it exposes internal kernel functions and state directly to userspace may constitute a serious security vulnerability if used for any reason other than testing. The header defines: - The FUZZ_TEST() macro for creating test targets. - The data structures required for the binary serialization format, which allows passing complex inputs from userspace. - The metadata structures for test targets, constraints and annotations, which are placed in dedicated ELF sections (.kfuzztest_*) for discovery. This patch only adds the public interface and build integration; no runtime logic is included. Signed-off-by: Ethan Graham --- arch/x86/kernel/vmlinux.lds.S | 22 ++ include/linux/kfuzztest.h | 537 ++++++++++++++++++++++++++++++++++ lib/Kconfig.debug | 1 + lib/Kconfig.kfuzztest | 20 ++ 4 files changed, 580 insertions(+) create mode 100644 include/linux/kfuzztest.h create mode 100644 lib/Kconfig.kfuzztest diff --git a/arch/x86/kernel/vmlinux.lds.S b/arch/x86/kernel/vmlinux.lds.S index 4fa0be732af10f..484e3e1ffb9f9e 100644 --- a/arch/x86/kernel/vmlinux.lds.S +++ b/arch/x86/kernel/vmlinux.lds.S @@ -112,6 +112,26 @@ ASSERT(__relocate_kernel_end - __relocate_kernel_start <= KEXEC_CONTROL_CODE_MAX #else #define KEXEC_RELOCATE_KERNEL #endif + +#ifdef CONFIG_KFUZZTEST +#define KFUZZTEST_TABLE \ + . = ALIGN(PAGE_SIZE); \ + __kfuzztest_targets_start = .; \ + KEEP(*(.kfuzztest_target)); \ + __kfuzztest_targets_end = .; \ + . = ALIGN(PAGE_SIZE); \ + __kfuzztest_constraints_start = .; \ + KEEP(*(.kfuzztest_constraint)); \ + __kfuzztest_constraints_end = .; \ + . = ALIGN(PAGE_SIZE); \ + __kfuzztest_annotations_start = .; \ + KEEP(*(.kfuzztest_annotation)); \ + __kfuzztest_annotations_end = .; + +#else /* CONFIG_KFUZZTEST */ +#define KFUZZTEST_TABLE +#endif /* CONFIG_KFUZZTEST */ + PHDRS { text PT_LOAD FLAGS(5); /* R_E */ data PT_LOAD FLAGS(6); /* RW_ */ @@ -199,6 +219,8 @@ SECTIONS CONSTRUCTORS KEXEC_RELOCATE_KERNEL + KFUZZTEST_TABLE + /* rarely changed data like cpu maps */ READ_MOSTLY_DATA(INTERNODE_CACHE_BYTES) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h new file mode 100644 index 00000000000000..067935060c5e45 --- /dev/null +++ b/include/linux/kfuzztest.h @@ -0,0 +1,537 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * 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 + +#include +#include +#include + +/** + * @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, 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. 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. + * + * For a detailed specification of the binary layout see the full documentation + * at: Documentation/dev-tools/kfuzztest.rst + */ + +/** + * 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 + * underlying type. + * @size: The size of this region in bytes. + */ +struct reloc_region { + uint32_t offset; + uint32_t size; +}; + +/** + * 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. + */ +struct reloc_region_array { + uint32_t num_regions; + struct reloc_region regions[]; +}; + +/** + * 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. + * @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; + uint32_t region_offset; + uint32_t value; +}; + +/** + * 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 + * 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 padding_size; + struct reloc_entry entries[]; +}; + +/** + * __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. + * @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. + * + * 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, + struct reloc_table **ret_reloc_table, + void **ret_payload_start, void **ret_payload_end); + +/** + * __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. + * @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 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). + * + * 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, + 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 inline 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].offset, regions->regions[i].size); + } + + pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", + 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, + 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; + ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, + size_t len, loff_t *off); +} __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"); + +/* clang-format off */ +/** + * 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. + * + * 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: + * + * - `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. + * + * - `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: + * + * // 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; + * }; + * + * // 3. Define the fuzz test using the FUZZ_TEST macro. + * FUZZ_TEST(process_data_fuzzer, struct process_data_inputs) + * { + * 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) \ + 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 = kfuzztest_write_cb_##test_name, \ + }; \ + static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, \ + const char __user *buf, \ + size_t len, loff_t *off) \ + { \ + void *payload_start, *payload_end, *buffer; \ + struct reloc_region_array *regions; \ + struct reloc_table *rt; \ + test_arg_type *arg; \ + int ret; \ + \ + buffer = kmalloc(len, GFP_KERNEL); \ + if (!buffer) \ + return -ENOMEM; \ + 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); \ + if (ret) \ + goto out; \ + __kfuzztest_debug_header(regions, rt, payload_start, \ + payload_end); \ + ret = __kfuzztest_relocate(regions, rt, payload_start, \ + payload_end); \ + if (ret) \ + goto out; \ + /* Call the fuzz logic on the provided written input. */ \ + arg = (test_arg_type *)payload_start; \ + kfuzztest_logic_##test_name(arg, regions); \ + ret = len; \ +out: \ + kfree(buffer); \ + return ret; \ + } \ + static void kfuzztest_logic_##test_name( \ + test_arg_type *arg, struct reloc_region_array *regions) + +enum kfuzztest_constraint_type { + EXPECT_EQ = 0, + EXPECT_NE, + EXPECT_LT, + EXPECT_LE, + EXPECT_GT, + EXPECT_GE, + EXPECT_IN_RANGE, +}; + +/** + * 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 + * check, they are primarily intended as a discoverable contract for userspace + * fuzzers. + * + * 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. + * + * For an example of how these constraints are used within a fuzz test, see the + * documentation for the FUZZ_TEST() macro. + * + * @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; + const char *field_name; + uintptr_t value1; + uintptr_t value2; + enum kfuzztest_constraint_type type; +} __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"); + +#define __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ + static struct kfuzztest_constraint __constraint_##arg_type##_##f \ + __attribute__((__section__(".kfuzztest_constraint"), \ + __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .value1 = (uintptr_t)val1, \ + .value2 = (uintptr_t)val2, \ + .type = tpe, \ + } + +#define KFUZZTEST_EXPECT_EQ(arg_type, field, val) \ + do { \ + if (arg->field != val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, \ + val, 0x0, EXPECT_EQ); \ + } while (0) + +#define KFUZZTEST_EXPECT_NE(arg_type, field, val) \ + do { \ + if (arg->field == val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ + 0x0, EXPECT_NE); \ + } while (0) + +#define KFUZZTEST_EXPECT_LT(arg_type, field, val) \ + do { \ + if (arg->field >= val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ + 0x0, EXPECT_LT); \ + } while (0) + +#define KFUZZTEST_EXPECT_LE(arg_type, field, val) \ + do { \ + if (arg->field > val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ + 0x0, EXPECT_LE); \ + } while (0) + +#define KFUZZTEST_EXPECT_GT(arg_type, field, val) \ + do { \ + if (arg->field <= val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ + 0x0, EXPECT_GT) \ + } while (0) + +#define KFUZZTEST_EXPECT_GE(arg_type, field, val) \ + do { \ + if (arg->field < val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ + 0x0, EXPECT_GE)` \ + } while (0) + +#define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) \ + KFUZZTEST_EXPECT_NE(arg_type, field, 0x0) + +#define KFUZZTEST_EXPECT_IN_RANGE(arg_type, field, lower_bound, upper_bound) \ + do { \ + if (arg->field < lower_bound || arg->field > upper_bound) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, lower_bound, \ + upper_bound, EXPECT_IN_RANGE) \ + } while (0) + +/** + * Annotations express attributes about structure fields that can't be easily + * 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 *` 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, + ATTRIBUTE_STRING, + ATTRIBUTE_ARRAY, +}; + +/** + * 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 + * 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; + const char *linked_field_name; + enum kfuzztest_annotation_attribute attrib; +} __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"), \ + __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .linked_field_name = #linked_field, \ + .attrib = attribute, \ + } + +/** + * Annotates a char* field as a string, which is the subset of char arrays that + * are null-terminated. + */ +#define KFUZZTEST_ANNOTATE_STRING(arg_type, field) \ + __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_STRING) + +/** + * Annotates a pointer field as an array, which is a contiguous memory region + * containing zero or more elements of the same type. + */ +#define KFUZZTEST_ANNOTATE_ARRAY(arg_type, field) \ + __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_ARRAY) + +/** + * Annotates arg_type.field as the length of arg_type.linked_field + */ +#define KFUZZTEST_ANNOTATE_LEN(arg_type, field, linked_field) \ + __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) + +#define KFUZZTEST_REGIONID_NULL U32_MAX + +/** 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) + +/* + * 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_POISON_SIZE __KASAN_GRANULE_SIZE + +#endif /* KFUZZTEST_H */ diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index ebe33181b6e6e0..5a33c34b107469 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -1040,6 +1040,7 @@ config MEM_ALLOC_PROFILING_DEBUG source "lib/Kconfig.kasan" source "lib/Kconfig.kfence" source "lib/Kconfig.kmsan" +source "lib/Kconfig.kfuzztest" endmenu # "Memory Debugging" diff --git a/lib/Kconfig.kfuzztest b/lib/Kconfig.kfuzztest new file mode 100644 index 00000000000000..6364ca8e78b168 --- /dev/null +++ b/lib/Kconfig.kfuzztest @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only + +config KFUZZTEST + bool "KFuzzTest - enable support for internal fuzz targets" + 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. + + WARNING: This exposes internal kernel functions directly to userspace + and must NEVER be enabled in production builds. From 27b335e706fda801c119596ef80b2d67637b3251 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 12:01:16 +0000 Subject: [PATCH 03/18] kfuzztest: implement core module and input processing Add the core runtime implementation for KFuzzTest. This includes the module initialization, and the logic for receiving and processing user-provided inputs through debugfs. On module load, the framework discovers all test targets by iterating over the .kfuzztest_target section, creating a corresponding debugfs directory with a write-only 'input' file for each of them. Writing to an 'input' file triggers the main fuzzing sequence: 1. The serialized input is copied from userspace into a kernel buffer. 2. The buffer is parsed to validate the region array and relocation table. 3. Pointers are patched based on the relocation entries, and in KASAN builds the inter-region padding is poisoned. 4. The resulting struct is passed to the user-defined test logic. Signed-off-by: Ethan Graham --- lib/Makefile | 2 + lib/kfuzztest/Makefile | 4 + lib/kfuzztest/main.c | 161 ++++++++++++++++++++++++++++++++++++ lib/kfuzztest/parse.c | 131 +++++++++++++++++++++++++++++ lib/kfuzztest/relocations.c | 104 +++++++++++++++++++++++ 5 files changed, 402 insertions(+) create mode 100644 lib/kfuzztest/Makefile create mode 100644 lib/kfuzztest/main.c create mode 100644 lib/kfuzztest/parse.c create mode 100644 lib/kfuzztest/relocations.c diff --git a/lib/Makefile b/lib/Makefile index c38582f187dd81..511c44ef4b19e9 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -354,6 +354,8 @@ obj-$(CONFIG_GENERIC_LIB_CMPDI2) += cmpdi2.o obj-$(CONFIG_GENERIC_LIB_UCMPDI2) += ucmpdi2.o obj-$(CONFIG_OBJAGG) += objagg.o +obj-$(CONFIG_KFUZZTEST) += kfuzztest/ + # pldmfw library obj-$(CONFIG_PLDMFW) += pldmfw/ diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile new file mode 100644 index 00000000000000..5d950946bf3e5b --- /dev/null +++ b/lib/kfuzztest/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 + +obj-$(CONFIG_KFUZZTEST) += kfuzztest.o +kfuzztest-objs := main.o relocations.o parse.o diff --git a/lib/kfuzztest/main.c b/lib/kfuzztest/main.c new file mode 100644 index 00000000000000..fccda1319fb0d4 --- /dev/null +++ b/lib/kfuzztest/main.c @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KFuzzTest core module initialization and debugfs interface. + * + * Copyright 2025 Google LLC + */ +#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[]; + +/** + * struct kfuzztest_dentry - A container for a debugfs dentry and its fops. + * @dentry: Pointer to the created debugfs dentry. + * @fops: The file_operations struct associated with this dentry. + * + * This simplifies state management by keeping a file's dentry and its + * operations bundled together. + */ +struct kfuzztest_dentry { + struct dentry *dentry; + struct file_operations fops; +}; + +/** + * struct kfuzztest_debugfs_state - Per-test-case debugfs state. + * @test_dir: The top-level debugfs directory for a single test case, e.g., + * /sys/kernel/debug/kfuzztest//. + * @input_dentry: The state for the "input" file, which is write-only. + * + * Wraps all debugfs components created for a single test case. + */ +struct kfuzztest_debugfs_state { + struct dentry *target_dir; + struct kfuzztest_dentry input_dentry; +}; + +/** + * struct kfuzztest_simple_fuzzer_state - Global state for the KFTF module. + * @kfuzztest_dir: The root debugfs directory, /sys/kernel/debug/kfuzztest/. + * @debugfs_state: A statically sized array holding the state for each + * registered test case. + */ +struct kfuzztest_state { + struct file_operations fops; + struct dentry *kfuzztest_dir; + struct kfuzztest_debugfs_state *debugfs_state; +}; + +/* Global static variable to hold all state for the module. */ +static struct kfuzztest_state state; + +const umode_t KFUZZTEST_INPUT_PERMS = 0222; + +/** + * kfuzztest_init - Initializes the debug filesystem for KFuzzTest. + * + * Each registered test in the ".kfuzztest" section gets its own subdirectory + * under "/sys/kernel/debug/kfuzztest/" with one files: + * - input: write-only file to send input to the fuzz driver + * + * Returns: + * 0 on success. + * -ENODEV or other error codes if debugfs creation fails. + */ +static int __init kfuzztest_init(void) +{ + const struct kfuzztest_target *targ; + int ret = 0; + int i = 0; + size_t num_test_cases; + + num_test_cases = __kfuzztest_targets_end - __kfuzztest_targets_start; + + state.debugfs_state = + kzalloc(num_test_cases * sizeof(struct kfuzztest_debugfs_state), + GFP_KERNEL); + if (!state.debugfs_state) + return -ENOMEM; + + /* Create the main "kfuzztest" directory in /sys/kernel/debug. */ + state.kfuzztest_dir = debugfs_create_dir("kfuzztest", NULL); + if (!state.kfuzztest_dir) { + pr_warn("KFuzzTest: could not create debugfs"); + return -ENODEV; + } + + 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. */ + state.debugfs_state[i].target_dir = + debugfs_create_dir(targ->name, state.kfuzztest_dir); + + if (!state.debugfs_state[i].target_dir) { + ret = -ENOMEM; + goto cleanup_failure; + } 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. */ + state.debugfs_state[i].input_dentry.fops = + (struct file_operations){ + .owner = THIS_MODULE, + .write = targ->write_input_cb, + }; + 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(state.debugfs_state[i].input_dentry.dentry)) { + ret = PTR_ERR( + state.debugfs_state[i].input_dentry.dentry); + goto cleanup_failure; + } + + pr_info("KFuzzTest: registered target %s", targ->name); + } + + return 0; + +cleanup_failure: + debugfs_remove_recursive(state.kfuzztest_dir); + return ret; +} + +static void __exit kfuzztest_exit(void) +{ + pr_info("KFuzzTest: exiting"); + if (!state.kfuzztest_dir) + return; + + debugfs_remove_recursive(state.kfuzztest_dir); + state.kfuzztest_dir = NULL; + + if (state.debugfs_state) { + kfree(state.debugfs_state); + state.debugfs_state = NULL; + } +} + +module_init(kfuzztest_init); +module_exit(kfuzztest_exit); diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c new file mode 100644 index 00000000000000..f19c8f1853e58a --- /dev/null +++ b/lib/kfuzztest/parse.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * KFuzzTest input parsing and validation. + * + * Copyright 2025 Google LLC + */ +#include + +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; + struct reloc_region reg, next_reg; + size_t usable_payload_size; + uint32_t region_end_offset; + struct reloc_entry reloc; + uint32_t i; + + if ((char *)payload_start > (char *)payload_end) + return false; + if (payload_size < KFUZZTEST_POISON_SIZE) + return false; + usable_payload_size = payload_size - KFUZZTEST_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; + + 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; + } + } + + 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) + 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) +{ + size_t reloc_entries_size, reloc_regions_size; + size_t reloc_table_size, regions_size; + 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; + + regions = input; + 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; + + curr_offset = regions_size; + if (curr_offset > input_size) + return -EINVAL; + if (input_size - curr_offset < sizeof(struct reloc_table)) + return -EINVAL; + + 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->padding_size, + &reloc_table_size)) + return -EINVAL; + + 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; + *ret_reloc_table = rt; + *ret_payload_start = payload_start; + *ret_payload_end = payload_end; + return 0; +} diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c new file mode 100644 index 00000000000000..ed188a25076721 --- /dev/null +++ b/lib/kfuzztest/relocations.c @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KFuzzTest input relocation and memory poisoning. + * + * Copyright 2025 Google LLC + */ +#include +#include + +#ifdef CONFIG_KASAN + +/** + * 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 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) +{ + 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; + size_t head_prefix_size; + + if (start_addr >= end_addr) + return; + + 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, __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, + __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) +{ + struct reloc_region reg, src, dst; + void *poison_start, *poison_end; + uintptr_t *ptr_location; + struct reloc_entry re; + size_t i; + + /* 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) + *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.offset); + } else + return -EINVAL; + } + + /* Poison the padding between regions. */ + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + + /* Points to the beginning of the inter-region padding */ + poison_start = payload_start + reg.offset + reg.size; + if (i < regions->num_regions - 1) + poison_end = + payload_start + regions->regions[i + 1].offset; + else + poison_end = payload_end; + + if ((char *)poison_end > (char *)payload_end) + return -EINVAL; + + kfuzztest_poison_range(poison_start, poison_end); + } + + /* Poison the padded area preceding the payload. */ + kfuzztest_poison_range((char *)payload_start - rt->padding_size, + payload_start); + return 0; +} From 67b5651f54c4f9697401afc83eee2eb3b85bc51c Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 12:12:36 +0000 Subject: [PATCH 04/18] kfuzztest: add ReST documentation Add Documentation/dev-tools/kfuzztest.rst and reference it in the dev-tools index. Signed-off-by: Ethan Graham --- Documentation/dev-tools/index.rst | 1 + Documentation/dev-tools/kfuzztest.rst | 187 ++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 Documentation/dev-tools/kfuzztest.rst diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst index 65c54b27a60b8d..ff453ac6292c9d 100644 --- a/Documentation/dev-tools/index.rst +++ b/Documentation/dev-tools/index.rst @@ -30,6 +30,7 @@ Documentation/process/debugging/index.rst kmemleak kcsan kfence + kfuzztest kselftest kunit/index ktap diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst new file mode 100644 index 00000000000000..92afad2a93c6b5 --- /dev/null +++ b/Documentation/dev-tools/kfuzztest.rst @@ -0,0 +1,187 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. Copyright 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. + +.. warning:: + KFuzzTest is a debugging and testing tool. It exposes internal kernel + functions to userspace with minimal sanitization and is designed for + use in controlled test environments only. It must **NEVER** be enabled + in production kernels. + +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 or other memory error detection tools. + 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. 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 90aaeb0ecf09426a24976995d4855156b45d7b61 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Fri, 8 Aug 2025 15:38:31 +0000 Subject: [PATCH 05/18] kfuzztest: add example fuzz targets Add two simple fuzz target examples to demonstrate the KFuzzTest API and provide basic self-tests for the framework. These examples showcase how a developer can define a fuzz target using the FUZZ_TEST(), constrain, and annotation macros, and serve as runtime sanity checks for the core logic. For example, they test that out-of-bounds memory accesses into poisoned padding regions are correctly detected in a KASAN build. These have been tested by writing syzkaller-generated inputs into their debugfs 'input' files and verifying that the correct KASAN reports were triggered. Signed-off-by: Ethan Graham --- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/examples.c | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 lib/kfuzztest/examples.c diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index 5d950946bf3e5b..ddf8ab63219ff4 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 := main.o relocations.o parse.o +kfuzztest-objs := main.o relocations.o parse.o examples.o diff --git a/lib/kfuzztest/examples.c b/lib/kfuzztest/examples.c new file mode 100644 index 00000000000000..6c709d25818fda --- /dev/null +++ b/lib/kfuzztest/examples.c @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains some KFuzzTest target examples. + * + * Copyright 2025 Google LLC + */ +#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 | + * + * 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) +{ + 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); + + 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; +}; + +/** + * 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; + + 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 980b07323967a7c78e6046502629792c9f8b2a05 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 09:09:37 +0000 Subject: [PATCH 06/18] kfuzztest: refactor as-per Marco's suggestions --- include/linux/kfuzztest.h | 309 ++++++++++++++---------------------- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/examples.c | 4 +- lib/kfuzztest/parse.c | 177 +++++++++++++++++---- lib/kfuzztest/relocations.c | 104 ------------ 5 files changed, 267 insertions(+), 329 deletions(-) delete mode 100644 lib/kfuzztest/relocations.c diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 067935060c5e45..f6ef6a222d75a9 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -14,6 +14,11 @@ #include #include +#define KFUZZTEST_HEADER_MAGIC (0xBFACE) +#define KFUZZTEST_V0 (0) +#define KFUZZTEST_GET_MAGIC(prefix) ((u32)(prefix & 0xFFFFFFFF)) +#define KFUZZTEST_GET_VERSION(prefix) ((u32)((prefix >> 32) & 0xFFFFFFFF)) + /** * @brief The KFuzzTest Input Serialization Format * @@ -28,7 +33,11 @@ * 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: + * The format consists of a prefix and three main components: + * 0. An 8-byte prefix: Contains KFUZZTEST_MAGIC in the four least significant + * bytes, and the version number in the 4 most significant bytes. The aim + * of this is to ensure backwards compatibility of userspace fuzzers in the + * event of future format changes. * 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, @@ -100,101 +109,53 @@ struct reloc_table { }; /** - * __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. - * @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. + * kfuzztest_parse_and_relocate - validate and relocate a KFuzzTest input * - * Returns: 0 on success, or a negative error code if the input is corrupted. + * @input: A buffer containing the serialized input for a fuzz target. + * @input_size: the size in bytes of the @input buffer. + * @arg_ret: return pointer for the test case's input structure. */ -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); - -/** - * __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. - * @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 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). - * - * 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, - void *payload_end); +int kfuzztest_parse_and_relocate(void *input, size_t input_size, void **arg_ret); /* * 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 inline void -__kfuzztest_debug_header(struct reloc_region_array *regions, - struct reloc_table *rt, void *payload_start, - void *payload_end) +__attribute__((unused)) static inline 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); + 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].offset, regions->regions[i].size); + pr_info(" region_%u: { start: 0x%x, size: 0x%x }", i, regions->regions[i].offset, + regions->regions[i].size); } - pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", - rt->num_entries, rt->padding_size, + pr_info("reloc_table: { num_entries = %u, padding = %u } @ offset 0x%lx", 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, - rt->entries[i].region_id, rt->entries[i].region_offset, - rt->entries[i].value); + 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, + 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; - ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, - size_t len, loff_t *off); + ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, size_t len, loff_t *off); } __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"); +static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target should have size 32"); -/* clang-format off */ /** * FUZZ_TEST - defines a KFuzzTest target * @@ -256,55 +217,41 @@ static_assert(sizeof(struct kfuzztest_target) == 32, * ret = process_data(arg->data, arg->len); * } */ -#define FUZZ_TEST(test_name, test_arg_type) \ - 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 = kfuzztest_write_cb_##test_name, \ - }; \ - static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, \ - const char __user *buf, \ - size_t len, loff_t *off) \ - { \ - void *payload_start, *payload_end, *buffer; \ - struct reloc_region_array *regions; \ - struct reloc_table *rt; \ - test_arg_type *arg; \ - int ret; \ - \ - buffer = kmalloc(len, GFP_KERNEL); \ - if (!buffer) \ - return -ENOMEM; \ - 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); \ - if (ret) \ - goto out; \ - __kfuzztest_debug_header(regions, rt, payload_start, \ - payload_end); \ - ret = __kfuzztest_relocate(regions, rt, payload_start, \ - payload_end); \ - if (ret) \ - goto out; \ - /* Call the fuzz logic on the provided written input. */ \ - arg = (test_arg_type *)payload_start; \ - kfuzztest_logic_##test_name(arg, regions); \ - ret = len; \ -out: \ - kfree(buffer); \ - return ret; \ - } \ - static void kfuzztest_logic_##test_name( \ - test_arg_type *arg, struct reloc_region_array *regions) +#define FUZZ_TEST(test_name, test_arg_type) \ + 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); \ + 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 = kfuzztest_write_cb_##test_name, \ + }; \ + static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, const char __user *buf, size_t len, \ + loff_t *off) \ + { \ + test_arg_type *arg; \ + void *buffer; \ + int ret; \ + \ + if (len < sizeof(u64)) \ + return -EINVAL; \ + buffer = kmalloc(len, GFP_KERNEL); \ + if (!buffer) \ + return -ENOMEM; \ + ret = simple_write_to_buffer(buffer, len, off, buf, len); \ + if (ret < 0) \ + goto out; \ + ret = kfuzztest_parse_and_relocate(buffer, len, (void **)&arg); \ + if (ret < 0) \ + goto out; \ + kfuzztest_logic_##test_name(arg); \ + ret = len; \ +out: \ + kfree(buffer); \ + return ret; \ + } \ + static void kfuzztest_logic_##test_name(test_arg_type *arg) enum kfuzztest_constraint_type { EXPECT_EQ = 0, @@ -354,77 +301,67 @@ struct kfuzztest_constraint { * 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"); - -#define __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ - static struct kfuzztest_constraint __constraint_##arg_type##_##f \ - __attribute__((__section__(".kfuzztest_constraint"), \ - __used__)) = { \ - .input_type = "struct " #arg_type, \ - .field_name = #field, \ - .value1 = (uintptr_t)val1, \ - .value2 = (uintptr_t)val2, \ - .type = tpe, \ +static_assert(sizeof(struct kfuzztest_constraint) == 64, "struct kfuzztest_constraint should have size 64"); + +#define __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ + static struct kfuzztest_constraint __constraint_##arg_type##_##f \ + __attribute__((__section__(".kfuzztest_constraint"), __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .value1 = (uintptr_t)val1, \ + .value2 = (uintptr_t)val2, \ + .type = tpe, \ } -#define KFUZZTEST_EXPECT_EQ(arg_type, field, val) \ - do { \ - if (arg->field != val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, \ - val, 0x0, EXPECT_EQ); \ +#define KFUZZTEST_EXPECT_EQ(arg_type, field, val) \ + do { \ + if (arg->field != val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_EQ); \ } while (0) -#define KFUZZTEST_EXPECT_NE(arg_type, field, val) \ - do { \ - if (arg->field == val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ - 0x0, EXPECT_NE); \ +#define KFUZZTEST_EXPECT_NE(arg_type, field, val) \ + do { \ + if (arg->field == val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_NE); \ } while (0) -#define KFUZZTEST_EXPECT_LT(arg_type, field, val) \ - do { \ - if (arg->field >= val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ - 0x0, EXPECT_LT); \ +#define KFUZZTEST_EXPECT_LT(arg_type, field, val) \ + do { \ + if (arg->field >= val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_LT); \ } while (0) -#define KFUZZTEST_EXPECT_LE(arg_type, field, val) \ - do { \ - if (arg->field > val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ - 0x0, EXPECT_LE); \ +#define KFUZZTEST_EXPECT_LE(arg_type, field, val) \ + do { \ + if (arg->field > val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_LE); \ } while (0) -#define KFUZZTEST_EXPECT_GT(arg_type, field, val) \ - do { \ - if (arg->field <= val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ - 0x0, EXPECT_GT) \ +#define KFUZZTEST_EXPECT_GT(arg_type, field, val) \ + do { \ + if (arg->field <= val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GT) \ } while (0) -#define KFUZZTEST_EXPECT_GE(arg_type, field, val) \ - do { \ - if (arg->field < val) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, \ - 0x0, EXPECT_GE)` \ +#define KFUZZTEST_EXPECT_GE(arg_type, field, val) \ + do { \ + if (arg->field < val) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GE)` \ } while (0) -#define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) \ - KFUZZTEST_EXPECT_NE(arg_type, field, 0x0) +#define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) KFUZZTEST_EXPECT_NE(arg_type, field, 0x0) -#define KFUZZTEST_EXPECT_IN_RANGE(arg_type, field, lower_bound, upper_bound) \ - do { \ - if (arg->field < lower_bound || arg->field > upper_bound) \ - return; \ - __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, lower_bound, \ - upper_bound, EXPECT_IN_RANGE) \ +#define KFUZZTEST_EXPECT_IN_RANGE(arg_type, field, lower_bound, upper_bound) \ + do { \ + if (arg->field < lower_bound || arg->field > upper_bound) \ + return; \ + __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, lower_bound, upper_bound, EXPECT_IN_RANGE) \ } while (0) /** @@ -480,32 +417,28 @@ struct kfuzztest_annotation { * 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"), \ - __used__)) = { \ - .input_type = "struct " #arg_type, \ - .field_name = #field, \ - .linked_field_name = #linked_field, \ - .attrib = attribute, \ +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"), __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .linked_field_name = #linked_field, \ + .attrib = attribute, \ } /** * Annotates a char* field as a string, which is the subset of char arrays that * are null-terminated. */ -#define KFUZZTEST_ANNOTATE_STRING(arg_type, field) \ - __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_STRING) +#define KFUZZTEST_ANNOTATE_STRING(arg_type, field) __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_STRING) /** * Annotates a pointer field as an array, which is a contiguous memory region * containing zero or more elements of the same type. */ -#define KFUZZTEST_ANNOTATE_ARRAY(arg_type, field) \ - __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_ARRAY) +#define KFUZZTEST_ANNOTATE_ARRAY(arg_type, field) __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_ARRAY) /** * Annotates arg_type.field as the length of arg_type.linked_field @@ -515,10 +448,6 @@ static_assert(sizeof(struct kfuzztest_annotation) == 32, #define KFUZZTEST_REGIONID_NULL U32_MAX -/** 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) - /* * 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 diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index ddf8ab63219ff4..011349a74375fe 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 := main.o relocations.o parse.o examples.o +kfuzztest-objs := main.o parse.o examples.o diff --git a/lib/kfuzztest/examples.c b/lib/kfuzztest/examples.c index 6c709d25818fda..a4d05f8e4bfd88 100644 --- a/lib/kfuzztest/examples.c +++ b/lib/kfuzztest/examples.c @@ -31,8 +31,8 @@ FUZZ_TEST(test_overflow_on_nested_buffer, struct nested_buffers) KFUZZTEST_ANNOTATE_LEN(nested_buffers, a_len, a); KFUZZTEST_ANNOTATE_LEN(nested_buffers, b_len, b); - 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)); + pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); + pr_info("b = [%px, %px)", arg->b, arg->b + arg->b_len); /* Ensure that all bytes in arg->b are accessible. */ for (i = 0; i < arg->b_len; i++) diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index f19c8f1853e58a..2a8303a7d5ac01 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -5,10 +5,101 @@ * Copyright 2025 Google LLC */ #include +#include -static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, - struct reloc_table *rt, - void *payload_start, void *payload_end) +#ifdef CONFIG_KASAN + +/** + * 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 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) +{ + 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; + size_t head_prefix_size; + + if (start_addr >= end_addr) + return; + + 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, __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, __KASAN_SLAB_REDZONE, + false); +} + +#else /* CONFIG_KASAN */ + +static inline void kfuzztest_poison_range(void *, void *) +{ +} + +#endif /* CONFIG_KASAN */ + +static int kfuzztest_relocate_v0(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, + void *payload_end) +{ + struct reloc_region reg, src, dst; + void *poison_start, *poison_end; + uintptr_t *ptr_location; + struct reloc_entry re; + size_t i; + + /* 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) + *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.offset); + } else + return -EINVAL; + } + + /* Poison the padding between regions. */ + for (i = 0; i < regions->num_regions; i++) { + reg = regions->regions[i]; + + /* Points to the beginning of the inter-region padding */ + poison_start = payload_start + reg.offset + reg.size; + if (i < regions->num_regions - 1) + poison_end = payload_start + regions->regions[i + 1].offset; + else + poison_end = payload_end; + + if ((char *)poison_end > (char *)payload_end) + return -EINVAL; + + kfuzztest_poison_range(poison_start, poison_end); + } + + /* Poison the padded area preceding the payload. */ + kfuzztest_poison_range((char *)payload_start - rt->padding_size, payload_start); + 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; struct reloc_region reg, next_reg; @@ -25,8 +116,7 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, for (i = 0; i < regions->num_regions; i++) { reg = regions->regions[i]; - if (check_add_overflow(reg.offset, reg.size, - ®ion_end_offset)) + if (check_add_overflow(reg.offset, reg.size, ®ion_end_offset)) return false; if ((size_t)region_end_offset > usable_payload_size) return false; @@ -39,15 +129,13 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, * Enforce the minimum poisonable gap between * consecutive regions. */ - if (reg.offset + reg.size + KFUZZTEST_POISON_SIZE > - next_reg.offset) + if (reg.offset + reg.size + KFUZZTEST_POISON_SIZE > next_reg.offset) return false; } } if (rt->padding_size < KFUZZTEST_POISON_SIZE) { - pr_info("validation failed because rt->padding_size = %u", - rt->padding_size); + pr_info("validation failed because rt->padding_size = %u", rt->padding_size); return false; } @@ -55,23 +143,20 @@ static bool __kfuzztest_input_is_valid(struct reloc_region_array *regions, reloc = rt->entries[i]; if (reloc.region_id >= regions->num_regions) return false; - if (reloc.value != KFUZZTEST_REGIONID_NULL && - reloc.value >= regions->num_regions) + 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) + 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) +static int kfuzztest_parse_input_v0(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) { size_t reloc_entries_size, reloc_regions_size; size_t reloc_table_size, regions_size; @@ -80,17 +165,13 @@ int __kfuzztest_parse_input(void *input, size_t input_size, struct reloc_table *rt; size_t curr_offset = 0; - if (input_size < - sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) + if (input_size < sizeof(struct reloc_region_array) + sizeof(struct reloc_table)) return -EINVAL; regions = input; - if (check_mul_overflow(regions->num_regions, - sizeof(struct reloc_region), - &reloc_regions_size)) + 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)) + if (check_add_overflow(sizeof(*regions), reloc_regions_size, ®ions_size)) return -EINVAL; curr_offset = regions_size; @@ -101,14 +182,11 @@ int __kfuzztest_parse_input(void *input, size_t input_size, rt = (struct reloc_table *)((char *)input + curr_offset); - if (check_mul_overflow((size_t)rt->num_entries, - sizeof(struct reloc_entry), &reloc_entries_size)) + 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)) + if (check_add_overflow(sizeof(*rt), reloc_entries_size, &reloc_table_size)) return -EINVAL; - if (check_add_overflow(reloc_table_size, rt->padding_size, - &reloc_table_size)) + if (check_add_overflow(reloc_table_size, rt->padding_size, &reloc_table_size)) return -EINVAL; if (check_add_overflow(curr_offset, reloc_table_size, &curr_offset)) @@ -119,8 +197,7 @@ int __kfuzztest_parse_input(void *input, size_t input_size, payload_start = (char *)input + curr_offset; payload_end = (char *)input + input_size; - if (!__kfuzztest_input_is_valid(regions, rt, payload_start, - payload_end)) + if (!kfuzztest_input_is_valid(regions, rt, payload_start, payload_end)) return -EINVAL; *ret_regions = regions; @@ -129,3 +206,39 @@ int __kfuzztest_parse_input(void *input, size_t input_size, *ret_payload_end = payload_end; return 0; } + +static int kfuzztest_parse_and_relocate_v0(void *input, size_t input_size, void **arg_ret) +{ + struct reloc_region_array *regions; + void *payload_start, *payload_end; + struct reloc_table *reloc_table; + int ret; + + ret = kfuzztest_parse_input_v0(input, input_size, ®ions, &reloc_table, &payload_start, &payload_end); + if (ret < 0) + return ret; + + ret = kfuzztest_relocate_v0(regions, reloc_table, payload_start, payload_end); + if (ret < 0) + return ret; + *arg_ret = payload_start; + return 0; +} + +int kfuzztest_parse_and_relocate(void *input, size_t input_size, void **arg_ret) +{ + u32 version, magic; + + if (input_size < sizeof(u64)) + return -EINVAL; + + version = KFUZZTEST_GET_VERSION(*(u64 *)input); + magic = KFUZZTEST_GET_MAGIC(*(u64 *)input); + + switch (version) { + case 0: + return kfuzztest_parse_and_relocate_v0(input + sizeof(u64), input_size - sizeof(u64), arg_ret); + } + + return -EINVAL; +} diff --git a/lib/kfuzztest/relocations.c b/lib/kfuzztest/relocations.c deleted file mode 100644 index ed188a25076721..00000000000000 --- a/lib/kfuzztest/relocations.c +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * KFuzzTest input relocation and memory poisoning. - * - * Copyright 2025 Google LLC - */ -#include -#include - -#ifdef CONFIG_KASAN - -/** - * 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 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) -{ - 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; - size_t head_prefix_size; - - if (start_addr >= end_addr) - return; - - 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, __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, - __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) -{ - struct reloc_region reg, src, dst; - void *poison_start, *poison_end; - uintptr_t *ptr_location; - struct reloc_entry re; - size_t i; - - /* 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) - *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.offset); - } else - return -EINVAL; - } - - /* Poison the padding between regions. */ - for (i = 0; i < regions->num_regions; i++) { - reg = regions->regions[i]; - - /* Points to the beginning of the inter-region padding */ - poison_start = payload_start + reg.offset + reg.size; - if (i < regions->num_regions - 1) - poison_end = - payload_start + regions->regions[i + 1].offset; - else - poison_end = payload_end; - - if ((char *)poison_end > (char *)payload_end) - return -EINVAL; - - kfuzztest_poison_range(poison_start, poison_end); - } - - /* Poison the padded area preceding the payload. */ - kfuzztest_poison_range((char *)payload_start - rt->padding_size, - payload_start); - return 0; -} From 08f33937ab3f2531c507f9420cb0dc727018fd35 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 09:10:25 +0000 Subject: [PATCH 07/18] kfuzztest: expose KASAN API instead of defining custom function We now expose kasan_poison_range as a KASAN API instead of offloading this to kfuzztest. --- include/linux/kasan.h | 44 +++++++++++++--------------------- include/linux/kfuzztest.h | 10 +------- lib/kfuzztest/parse.c | 50 ++------------------------------------- mm/kasan/kasan.h | 28 ++++++++++++++++++++++ mm/kasan/shadow.c | 31 ++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 85 deletions(-) diff --git a/include/linux/kasan.h b/include/linux/kasan.h index 508d8a2d5ad613..09baeb6c9f4d3a 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -102,6 +102,21 @@ static inline bool kasan_has_integrated_init(void) } #ifdef CONFIG_KASAN + +/** + * kasan_poison_range - poison the memory range [start, start + size) + * + * The exact behavior is subject to alignment with KASAN_GRANULE_SIZE, defined + * in . + * + * - 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 + * the end of the range. Any remaining bytes in a final partial granule are + * ignored. + */ +void kasan_poison_range(const void *start, size_t size); + void __kasan_unpoison_range(const void *addr, size_t size); static __always_inline void kasan_unpoison_range(const void *addr, size_t size) { @@ -126,34 +141,6 @@ 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 */ - -/** - * 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); - void __kasan_poison_slab(struct slab *slab); static __always_inline void kasan_poison_slab(struct slab *slab) { @@ -430,6 +417,7 @@ static __always_inline bool kasan_check_byte(const void *addr) #else /* CONFIG_KASAN */ +static inline void kasan_poison_range(const void *start, size_t size) {} static inline void kasan_unpoison_range(const void *address, size_t size) {} static inline void kasan_poison_pages(struct page *page, unsigned int order, bool init) {} diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index f6ef6a222d75a9..69b590d8e3bc2a 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -448,19 +448,11 @@ static_assert(sizeof(struct kfuzztest_annotation) == 32, "struct kfuzztest_annot #define KFUZZTEST_REGIONID_NULL U32_MAX -/* - * 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_POISON_SIZE __KASAN_GRANULE_SIZE +#define KFUZZTEST_POISON_SIZE 0x8 #endif /* KFUZZTEST_H */ diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 2a8303a7d5ac01..c9171511aac3c1 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -7,52 +7,6 @@ #include #include -#ifdef CONFIG_KASAN - -/** - * 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 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) -{ - 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; - size_t head_prefix_size; - - if (start_addr >= end_addr) - return; - - 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, __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, __KASAN_SLAB_REDZONE, - false); -} - -#else /* CONFIG_KASAN */ - -static inline void kfuzztest_poison_range(void *, void *) -{ -} - -#endif /* CONFIG_KASAN */ - static int kfuzztest_relocate_v0(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) { @@ -90,11 +44,11 @@ static int kfuzztest_relocate_v0(struct reloc_region_array *regions, struct relo if ((char *)poison_end > (char *)payload_end) return -EINVAL; - kfuzztest_poison_range(poison_start, poison_end); + kasan_poison_range(poison_start, poison_end - poison_start); } /* Poison the padded area preceding the payload. */ - kfuzztest_poison_range((char *)payload_start - rt->padding_size, payload_start); + kasan_poison_range((char *)payload_start - rt->padding_size, rt->padding_size); return 0; } diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h index c4a478f2d22e42..129178be5e6492 100644 --- a/mm/kasan/kasan.h +++ b/mm/kasan/kasan.h @@ -499,6 +499,15 @@ 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 @@ -516,6 +525,25 @@ 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) diff --git a/mm/kasan/shadow.c b/mm/kasan/shadow.c index d2c70cd2afb1de..a1b6bfb35f07f9 100644 --- a/mm/kasan/shadow.c +++ b/mm/kasan/shadow.c @@ -147,6 +147,37 @@ void kasan_poison(const void *addr, size_t size, u8 value, bool init) } EXPORT_SYMBOL_GPL(kasan_poison); +void kasan_poison_range(const void *start, size_t size) +{ + void *end = (char *)start + size; + uintptr_t start_addr = (uintptr_t)start; + uintptr_t head_granule_start; + uintptr_t poison_body_start; + uintptr_t poison_body_end; + size_t head_prefix_size; + uintptr_t end_addr; + + end_addr = ALIGN_DOWN((uintptr_t)end, KASAN_GRANULE_SIZE); + if (start_addr >= end_addr) + return; + + 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, 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, + KASAN_SLAB_REDZONE, false); +} +EXPORT_SYMBOL(kasan_poison_range); + #ifdef CONFIG_KASAN_GENERIC void kasan_poison_last_granule(const void *addr, size_t size) { From 113cca0f3212a5dbe212eb21a556901fd38455b1 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 10:02:27 +0000 Subject: [PATCH 08/18] kfuzztest: return error when magic is incorrect --- lib/kfuzztest/parse.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index c9171511aac3c1..5f41811d1a039c 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -188,6 +188,8 @@ int kfuzztest_parse_and_relocate(void *input, size_t input_size, void **arg_ret) version = KFUZZTEST_GET_VERSION(*(u64 *)input); magic = KFUZZTEST_GET_MAGIC(*(u64 *)input); + if (magic != KFUZZTEST_HEADER_MAGIC) + return -EINVAL; switch (version) { case 0: From 97a7e5da3c56ca4d55adf973d618676db58af591 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 10:12:23 +0000 Subject: [PATCH 09/18] kfuzztest: move static assertions to .c file, update attribute syntax Use __used and __section instead of __attribute__(()). --- include/linux/kfuzztest.h | 59 +++++++++++++-------------------------- lib/kfuzztest/parse.c | 8 ++++++ 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 69b590d8e3bc2a..459bf486b85d94 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -150,12 +150,6 @@ struct kfuzztest_target { ssize_t (*write_input_cb)(struct file *filp, const char __user *buf, size_t len, loff_t *off); } __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"); - /** * FUZZ_TEST - defines a KFuzzTest target * @@ -221,12 +215,11 @@ static_assert(sizeof(struct kfuzztest_target) == 32, "struct kfuzztest_target sh 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); \ - 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 = kfuzztest_write_cb_##test_name, \ - }; \ + const struct kfuzztest_target __fuzz_test__##test_name __section(".kfuzztest_target") __used = { \ + .name = #test_name, \ + .arg_type_name = #test_arg_type, \ + .write_input_cb = kfuzztest_write_cb_##test_name, \ + }; \ static ssize_t kfuzztest_write_cb_##test_name(struct file *filp, const char __user *buf, size_t len, \ loff_t *off) \ { \ @@ -297,20 +290,14 @@ struct kfuzztest_constraint { enum kfuzztest_constraint_type type; } __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"); - -#define __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ - static struct kfuzztest_constraint __constraint_##arg_type##_##f \ - __attribute__((__section__(".kfuzztest_constraint"), __used__)) = { \ - .input_type = "struct " #arg_type, \ - .field_name = #field, \ - .value1 = (uintptr_t)val1, \ - .value2 = (uintptr_t)val2, \ - .type = tpe, \ +#define __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ + static struct kfuzztest_constraint __constraint_##arg_type##_##field __section(".kfuzztest_constraint") \ + __used = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .value1 = (uintptr_t)val1, \ + .value2 = (uintptr_t)val2, \ + .type = tpe, \ } #define KFUZZTEST_EXPECT_EQ(arg_type, field, val) \ @@ -413,19 +400,13 @@ struct kfuzztest_annotation { enum kfuzztest_annotation_attribute attrib; } __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"), __used__)) = { \ - .input_type = "struct " #arg_type, \ - .field_name = #field, \ - .linked_field_name = #linked_field, \ - .attrib = attribute, \ +#define __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, attribute) \ + static struct kfuzztest_annotation __annotation_##arg_type##_##field __section(".kfuzztest_annotation") \ + __used = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .linked_field_name = #linked_field, \ + .attrib = attribute, \ } /** diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 5f41811d1a039c..3370291e9a64a7 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -7,6 +7,14 @@ #include #include +/* + * 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"); +static_assert(sizeof(struct kfuzztest_constraint) == 64, "struct kfuzztest_constraint should have size 64"); +static_assert(sizeof(struct kfuzztest_annotation) == 32, "struct kfuzztest_annotation should have size 32"); + static int kfuzztest_relocate_v0(struct reloc_region_array *regions, struct reloc_table *rt, void *payload_start, void *payload_end) { From 2734f4241c1cb3b4b9380ca947ab305e947b5c11 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 10:15:55 +0000 Subject: [PATCH 10/18] kfuzztest: remove = 0 from enums --- include/linux/kfuzztest.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 459bf486b85d94..08502017809552 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -247,7 +247,7 @@ out: static void kfuzztest_logic_##test_name(test_arg_type *arg) enum kfuzztest_constraint_type { - EXPECT_EQ = 0, + EXPECT_EQ, EXPECT_NE, EXPECT_LT, EXPECT_LE, @@ -364,7 +364,7 @@ struct kfuzztest_constraint { * past the end of a non-null-terminated buffer. */ enum kfuzztest_annotation_attribute : uint8_t { - ATTRIBUTE_LEN = 0, + ATTRIBUTE_LEN, ATTRIBUTE_STRING, ATTRIBUTE_ARRAY, }; From be6900b293d078cd5774d089079c1d3a344b6d50 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 10:21:02 +0000 Subject: [PATCH 11/18] kfuzztest: move KConfig in KConfig.debug and to kfuzztest dir --- lib/Kconfig.debug | 2 +- lib/{Kconfig.kfuzztest => kfuzztest/Kconfig} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/{Kconfig.kfuzztest => kfuzztest/Kconfig} (96%) diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 5a33c34b107469..3542e94204c8bc 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -1040,7 +1040,6 @@ config MEM_ALLOC_PROFILING_DEBUG source "lib/Kconfig.kasan" source "lib/Kconfig.kfence" source "lib/Kconfig.kmsan" -source "lib/Kconfig.kfuzztest" endmenu # "Memory Debugging" @@ -1948,6 +1947,7 @@ endmenu menu "Kernel Testing and Coverage" source "lib/kunit/Kconfig" +source "lib/kfuzztest/Kconfig" config NOTIFIER_ERROR_INJECTION tristate "Notifier error injection" diff --git a/lib/Kconfig.kfuzztest b/lib/kfuzztest/Kconfig similarity index 96% rename from lib/Kconfig.kfuzztest rename to lib/kfuzztest/Kconfig index 6364ca8e78b168..f9fb5abf8d27db 100644 --- a/lib/Kconfig.kfuzztest +++ b/lib/kfuzztest/Kconfig @@ -2,7 +2,7 @@ config KFUZZTEST bool "KFuzzTest - enable support for internal fuzz targets" - depends on DEBUG_FS + depends on DEBUG_FS && DEBUG_KERNEL help Enables support for the kernel fuzz testing framework (KFuzzTest), an interface for exposing internal kernel functions to a userspace fuzzing From fc89ccf048a6d7eced668b294993ff30afe0fd3d Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 10:27:54 +0000 Subject: [PATCH 12/18] kfuzztest: update docs to use kernel style comments --- Documentation/dev-tools/kfuzztest.rst | 50 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst index 92afad2a93c6b5..f8282311702136 100644 --- a/Documentation/dev-tools/kfuzztest.rst +++ b/Documentation/dev-tools/kfuzztest.rst @@ -60,36 +60,48 @@ The following example illustrates how to create a fuzz target for a function .. 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. + /* + * 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. + /* + * 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. + /* + * 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. + /* + * 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 or other memory error detection tools. + /* + * 5. Call the kernel function with the provided inputs. + * 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); } From d4c7928684f71d1f970810c2943d8ff8e78864c7 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 12:10:23 +0000 Subject: [PATCH 13/18] kfuzztest: move examples /lib/kfuzztest to /samples --- lib/kfuzztest/Makefile | 2 +- lib/kfuzztest/examples.c | 75 ------------------- samples/Kconfig | 7 ++ samples/Makefile | 1 + samples/kfuzztest/Makefile | 3 + samples/kfuzztest/overflow_on_nested_buffer.c | 52 +++++++++++++ samples/kfuzztest/underflow_on_buffer.c | 41 ++++++++++ 7 files changed, 105 insertions(+), 76 deletions(-) delete mode 100644 lib/kfuzztest/examples.c create mode 100644 samples/kfuzztest/Makefile create mode 100644 samples/kfuzztest/overflow_on_nested_buffer.c create mode 100644 samples/kfuzztest/underflow_on_buffer.c diff --git a/lib/kfuzztest/Makefile b/lib/kfuzztest/Makefile index 011349a74375fe..142d16007eea98 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 := main.o parse.o examples.o +kfuzztest-objs := main.o parse.o diff --git a/lib/kfuzztest/examples.c b/lib/kfuzztest/examples.c deleted file mode 100644 index a4d05f8e4bfd88..00000000000000 --- a/lib/kfuzztest/examples.c +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * This file contains some KFuzzTest target examples. - * - * Copyright 2025 Google LLC - */ -#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 | - * - * 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) -{ - 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); - - pr_info("a = [%px, %px)", arg->a, arg->a + arg->a_len); - pr_info("b = [%px, %px)", arg->b, arg->b + arg->b_len); - - /* 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; -}; - -/** - * 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; - - 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)); -} diff --git a/samples/Kconfig b/samples/Kconfig index ffef9995020668..4be51a21d01079 100644 --- a/samples/Kconfig +++ b/samples/Kconfig @@ -321,6 +321,13 @@ config SAMPLE_HUNG_TASK if 2 or more processes read the same file concurrently, it will be detected by the hung_task watchdog. +config SAMPLE_KFUZZTEST + bool "Build KFuzzTest sample targets" + depends on KFUZZTEST + help + Build KFuzzTest sample targets that serve as selftests for input + deserialization and inter-region redzone poisoning logic. + source "samples/rust/Kconfig" source "samples/damon/Kconfig" diff --git a/samples/Makefile b/samples/Makefile index 07641e177bd8bb..3a0e7f744f445d 100644 --- a/samples/Makefile +++ b/samples/Makefile @@ -44,4 +44,5 @@ obj-$(CONFIG_SAMPLE_DAMON_WSSE) += damon/ obj-$(CONFIG_SAMPLE_DAMON_PRCL) += damon/ obj-$(CONFIG_SAMPLE_DAMON_MTIER) += damon/ obj-$(CONFIG_SAMPLE_HUNG_TASK) += hung_task/ +obj-$(CONFIG_SAMPLE_KFUZZTEST) += kfuzztest/ obj-$(CONFIG_SAMPLE_TSM_MR) += tsm-mr/ diff --git a/samples/kfuzztest/Makefile b/samples/kfuzztest/Makefile new file mode 100644 index 00000000000000..4f8709876c9e2d --- /dev/null +++ b/samples/kfuzztest/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only + +obj-$(CONFIG_SAMPLE_KFUZZTEST) += overflow_on_nested_buffer.o underflow_on_buffer.o diff --git a/samples/kfuzztest/overflow_on_nested_buffer.c b/samples/kfuzztest/overflow_on_nested_buffer.c new file mode 100644 index 00000000000000..8b4bab1d6d4a8b --- /dev/null +++ b/samples/kfuzztest/overflow_on_nested_buffer.c @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains a KFuzzTest example target that ensures that a buffer + * overflow on a nested region triggers a KASAN OOB access report. + * + * Copyright 2025 Google LLC + */ +#include + +static void overflow_on_nested_buffer(const char *a, size_t a_len, const char *b, size_t b_len) +{ + size_t i; + pr_info("a = [%px, %px)", a, a + a_len); + pr_info("b = [%px, %px)", b, b + b_len); + + /* Ensure that all bytes in arg->b are accessible. */ + for (i = 0; i < b_len; i++) + READ_ONCE(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 <= a_len; i++) + READ_ONCE(a[i]); +} + +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 | + * + * 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) +{ + 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); + + overflow_on_nested_buffer(arg->a, arg->a_len, arg->b, arg->b_len); +} diff --git a/samples/kfuzztest/underflow_on_buffer.c b/samples/kfuzztest/underflow_on_buffer.c new file mode 100644 index 00000000000000..fbe214274037a1 --- /dev/null +++ b/samples/kfuzztest/underflow_on_buffer.c @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This file contains a KFuzzTest example target that ensures that a buffer + * underflow on a region triggers a KASAN OOB access report. + * + * Copyright 2025 Google LLC + */ +#include + +static void underflow_on_buffer(char *buf, size_t buflen) +{ + size_t i; + + pr_info("buf = [%px, %px)", buf, buf + buflen); + + /* First ensure that all bytes in arg->b are accessible. */ + for (i = 0; i < buflen; i++) + READ_ONCE(buf[i]); + /* + * Provoke a buffer overflow on the first byte preceding b, triggering + * a KASAN report. + */ + READ_ONCE(*((char *)buf - 1)); +} + +struct some_buffer { + char *buf; + 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) +{ + KFUZZTEST_EXPECT_NOT_NULL(some_buffer, buf); + KFUZZTEST_ANNOTATE_LEN(some_buffer, buflen, buf); + + underflow_on_buffer(arg->buf, arg->buflen); +} From 05b59b5cbb216f1b91117a01c9f8743e41b972ad Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 12:12:44 +0000 Subject: [PATCH 14/18] Documentation: move kfuzzetest position in index --- Documentation/dev-tools/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst index ff453ac6292c9d..00ccc4da003be7 100644 --- a/Documentation/dev-tools/index.rst +++ b/Documentation/dev-tools/index.rst @@ -30,9 +30,9 @@ Documentation/process/debugging/index.rst kmemleak kcsan kfence - kfuzztest kselftest kunit/index + kfuzztest ktap checkuapi gpio-sloppy-logic-analyzer From 072d9450c65a17ea38cdb8c909560be99f04431e Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 12:16:34 +0000 Subject: [PATCH 15/18] kfuzztest: add example fuzz targets for real kernel functions --- crypto/asymmetric_keys/pkcs7_parser.c | 15 ++++++++++++++ crypto/rsa_helper.c | 29 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/crypto/asymmetric_keys/pkcs7_parser.c b/crypto/asymmetric_keys/pkcs7_parser.c index 423d13c475452c..e8477f8b0eafbf 100644 --- a/crypto/asymmetric_keys/pkcs7_parser.c +++ b/crypto/asymmetric_keys/pkcs7_parser.c @@ -13,6 +13,7 @@ #include #include #include +#include #include "pkcs7_parser.h" #include "pkcs7.asn1.h" @@ -169,6 +170,20 @@ struct pkcs7_message *pkcs7_parse_message(const void *data, size_t datalen) } EXPORT_SYMBOL_GPL(pkcs7_parse_message); +struct pkcs7_parse_message_arg { + const void *data; + size_t datalen; +}; + +FUZZ_TEST(test_pkcs7_parse_message, struct pkcs7_parse_message_arg) +{ + KFUZZTEST_EXPECT_NOT_NULL(pkcs7_parse_message_arg, data); + KFUZZTEST_ANNOTATE_LEN(pkcs7_parse_message_arg, datalen, data); + KFUZZTEST_EXPECT_LE(pkcs7_parse_message_arg, datalen, 16 * PAGE_SIZE); + + pkcs7_parse_message(arg->data, arg->datalen); +} + /** * pkcs7_get_content_data - Get access to the PKCS#7 content * @pkcs7: The preparsed PKCS#7 message to access diff --git a/crypto/rsa_helper.c b/crypto/rsa_helper.c index 94266f29049c92..79b7ddc7c48d2c 100644 --- a/crypto/rsa_helper.c +++ b/crypto/rsa_helper.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "rsapubkey.asn1.h" #include "rsaprivkey.asn1.h" @@ -166,6 +167,20 @@ int rsa_parse_pub_key(struct rsa_key *rsa_key, const void *key, } EXPORT_SYMBOL_GPL(rsa_parse_pub_key); +struct rsa_parse_pub_key_arg { + const void *key; + size_t key_len; +}; + +FUZZ_TEST(test_rsa_parse_pub_key, struct rsa_parse_pub_key_arg) +{ + KFUZZTEST_EXPECT_NOT_NULL(rsa_parse_pub_key_arg, key); + KFUZZTEST_EXPECT_LE(rsa_parse_pub_key_arg, key_len, 16 * PAGE_SIZE); + + struct rsa_key out; + rsa_parse_pub_key(&out, arg->key, arg->key_len); +} + /** * rsa_parse_priv_key() - decodes the BER encoded buffer and stores in the * provided struct rsa_key, pointers to the raw key @@ -184,3 +199,17 @@ int rsa_parse_priv_key(struct rsa_key *rsa_key, const void *key, return asn1_ber_decoder(&rsaprivkey_decoder, rsa_key, key, key_len); } EXPORT_SYMBOL_GPL(rsa_parse_priv_key); + +struct rsa_parse_priv_key_arg { + const void *key; + size_t key_len; +}; + +FUZZ_TEST(test_rsa_parse_priv_key, struct rsa_parse_priv_key_arg) +{ + KFUZZTEST_EXPECT_NOT_NULL(rsa_parse_priv_key_arg, key); + KFUZZTEST_EXPECT_LE(rsa_parse_priv_key_arg, key_len, 16 * PAGE_SIZE); + + struct rsa_key out; + rsa_parse_priv_key(&out, arg->key, arg->key_len); +} From fbf6555b4424a1fed1a6a3a71d74f976fab57d53 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 12:25:48 +0000 Subject: [PATCH 16/18] kfuzztest: remove a redundant check on input length --- include/linux/kfuzztest.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 08502017809552..3c4c9a23807d11 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -227,8 +227,6 @@ struct kfuzztest_target { void *buffer; \ int ret; \ \ - if (len < sizeof(u64)) \ - return -EINVAL; \ buffer = kmalloc(len, GFP_KERNEL); \ if (!buffer) \ return -ENOMEM; \ From ed22eef61ca4c33fcf799bf9b1524f31273fa3b3 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 12:42:02 +0000 Subject: [PATCH 17/18] kfuzztest: add kerneldoc comments for annotations and constraints --- include/linux/kfuzztest.h | 86 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 3c4c9a23807d11..8c3da9a4607f99 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -298,6 +298,13 @@ struct kfuzztest_constraint { .type = tpe, \ } +/** + * KFUZZTEST_EXPECT_EQ - constrain a field to be equal to a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable + * @val: a value of the same type as @arg_type.@field + */ #define KFUZZTEST_EXPECT_EQ(arg_type, field, val) \ do { \ if (arg->field != val) \ @@ -305,6 +312,13 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_EQ); \ } while (0) +/** + * KFUZZTEST_EXPECT_NE - constrain a field to be not equal to a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_NE(arg_type, field, val) \ do { \ if (arg->field == val) \ @@ -312,6 +326,13 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_NE); \ } while (0) +/** + * KFUZZTEST_EXPECT_LT - constrain a field to be less than a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_LT(arg_type, field, val) \ do { \ if (arg->field >= val) \ @@ -319,6 +340,13 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_LT); \ } while (0) +/** + * KFUZZTEST_EXPECT_LE - constrain a field to be less than or equal to a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_LE(arg_type, field, val) \ do { \ if (arg->field > val) \ @@ -326,6 +354,13 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_LE); \ } while (0) +/** + * KFUZZTEST_EXPECT_GT - constrain a field to be greater than a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_GT(arg_type, field, val) \ do { \ if (arg->field <= val) \ @@ -333,6 +368,13 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GT) \ } while (0) +/** + * KFUZZTEST_EXPECT_GE - constrain a field to be greater than or equal to a value + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_GE(arg_type, field, val) \ do { \ if (arg->field < val) \ @@ -340,8 +382,23 @@ struct kfuzztest_constraint { __KFUZZTEST_DEFINE_CONSTRAINT(arg_type, field, val, 0x0, EXPECT_GE)` \ } while (0) -#define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) KFUZZTEST_EXPECT_NE(arg_type, field, 0x0) +/** + * KFUZZTEST_EXPECT_GE - constrain a pointer field to be non-NULL + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @val: a value of the same type as @arg_type.@field. + */ +#define KFUZZTEST_EXPECT_NOT_NULL(arg_type, field) KFUZZTEST_EXPECT_NE(arg_type, field, NULL) +/** + * KFUZZTEST_EXPECT_IN_RANGE - constrain a field to be within a range + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: some field that is comparable. + * @lower_bound: a lower bound of the same type as @arg_type.@field. + * @upper_bound: an upper bound of the same type as @arg_type.@field. + */ #define KFUZZTEST_EXPECT_IN_RANGE(arg_type, field, lower_bound, upper_bound) \ do { \ if (arg->field < lower_bound || arg->field > upper_bound) \ @@ -408,19 +465,36 @@ struct kfuzztest_annotation { } /** - * Annotates a char* field as a string, which is the subset of char arrays that - * are null-terminated. + * KFUZZTEST_ANNOTATE_STRING - annotate a char* field as a C string + * + * We define a C string as a sequence of non-zero characters followed by exactly + * one null terminator. + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: the name of the field to annotate. */ #define KFUZZTEST_ANNOTATE_STRING(arg_type, field) __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_STRING) /** - * Annotates a pointer field as an array, which is a contiguous memory region - * containing zero or more elements of the same type. + * KFUZZTEST_ANNOTATE_ARRAY - annotate a pointer as an array + * + * We define an array as a contiguous memory region containing zero or more + * elements of the same type. + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: the name of the field to annotate. */ #define KFUZZTEST_ANNOTATE_ARRAY(arg_type, field) __KFUZZTEST_ANNOTATE(arg_type, field, NULL, ATTRIBUTE_ARRAY) /** - * Annotates arg_type.field as the length of arg_type.linked_field + * KFUZZTEST_ANNOTATE_LEN - annotate a field as the length of another + * + * This expresses the relationship `arg_type.field == len(linked_field)`, where + * `linked_field` is an array. + * + * @arg_type: name of the input structure, without the leading "struct ". + * @field: the name of the field to annotate. + * @linked_field: the name of an array field with length @field. */ #define KFUZZTEST_ANNOTATE_LEN(arg_type, field, linked_field) \ __KFUZZTEST_ANNOTATE(arg_type, field, linked_field, ATTRIBUTE_LEN) From 48888dee765f3ddbee5983aef488601ffbe3b22a Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Tue, 12 Aug 2025 13:45:01 +0000 Subject: [PATCH 18/18] kfuzztest: update how 8-byte version header is accessed in input --- Documentation/dev-tools/kfuzztest.rst | 62 ++++++++++++++++++++++----- include/linux/kfuzztest.h | 15 +++---- lib/kfuzztest/parse.c | 8 ++-- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/Documentation/dev-tools/kfuzztest.rst b/Documentation/dev-tools/kfuzztest.rst index f8282311702136..ae5f76dcaff913 100644 --- a/Documentation/dev-tools/kfuzztest.rst +++ b/Documentation/dev-tools/kfuzztest.rst @@ -1,11 +1,12 @@ .. SPDX-License-Identifier: GPL-2.0 .. Copyright 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. @@ -34,21 +35,30 @@ The framework consists of four main components: in production kernels. 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. +which depends on ``CONFIG_DEBUGFS`` for receiving userspace inputs, and +``CONFIG_DEBUG_KERNEL`` as an additional guardrail for preventing KFuzzTest +from finding its way into a production build accidentally. + +The KFuzzTest sample fuzz targets can be built in with +``CONFIG_SAMPLE_KFUZZTEST``. + +KFuzzTest currently only supports code that is built into the kernel, as the +core module's startup process discovers fuzz targets, constraints, and +annotations from a dedicated ELF section during startup. 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 @@ -120,7 +130,7 @@ KFuzzTest provides two families of macros to improve the quality of fuzzing: 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``. @@ -130,15 +140,24 @@ 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.:: +An input is first prefixed by an 8-byte header containing a magic value in the +first four bytes, defined as ``KFUZZTEST_HEADER_MAGIC`` in +```, and a version number in the subsequent four +bytes. + +Version 0 +--------- + +In version 0 (i.e., when the version number in the 8-byte header is equal to 0), +the input 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 @@ -163,8 +182,15 @@ 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. +To satisfy C language alignment requirements and prevent potential hardware +faults, the memory address of each region's data must be correctly aligned for +the type it represents. The framework allocates a base buffer that is suitably +aligned for any C type. Therefore, the userspace tool that generates the input +is responsible for calculating each region's offset within the payload to ensure +this alignment is maintained. + Relocation Table -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^ The relocation table provides the instructions for the kernel to "hydrate" the payload by patching pointer fields. It contains an array of @@ -180,8 +206,22 @@ specifying: This table also specifies the amount of padding between its end and the start of the payload, which should be at least 8 bytes. +.. code-block:: c + + struct reloc_entry { + uint32_t region_id; + uint32_t region_offset; + uint32_t value; + }; + + struct reloc_table { + uint32_t num_entries; + uint32_t padding_size; + struct reloc_entry entries[]; + }; + Payload -~~~~~~~ +^^^^^^^ The payload contains the raw binary data for all regions, concatenated together according to their specified offsets. diff --git a/include/linux/kfuzztest.h b/include/linux/kfuzztest.h index 8c3da9a4607f99..57d943845fbf21 100644 --- a/include/linux/kfuzztest.h +++ b/include/linux/kfuzztest.h @@ -16,8 +16,6 @@ #define KFUZZTEST_HEADER_MAGIC (0xBFACE) #define KFUZZTEST_V0 (0) -#define KFUZZTEST_GET_MAGIC(prefix) ((u32)(prefix & 0xFFFFFFFF)) -#define KFUZZTEST_GET_VERSION(prefix) ((u32)((prefix >> 32) & 0xFFFFFFFF)) /** * @brief The KFuzzTest Input Serialization Format @@ -34,19 +32,18 @@ * regions are poisoned by KFuzzTest to ensure that KASAN catches OOB accesses. * * The format consists of a prefix and three main components: - * 0. An 8-byte prefix: Contains KFUZZTEST_MAGIC in the four least significant - * bytes, and the version number in the 4 most significant bytes. The aim - * of this is to ensure backwards compatibility of userspace fuzzers in the - * event of future format changes. - * 1. A reloc_region_array: Defines the memory layout of the target structure + * 1. An 8-byte header: Contains KFUZZTEST_MAGIC in the first 4 bytes, and the + * version number in the subsequent 4 bytes. This ensures backwards + * compatibility in the event of future format changes. + * 2. 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. The region descriptors should be * ordered by offset ascending. - * 2. A reloc_table: Provides "linking" instructions that tell the kernel how + * 3. 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 + * 4. 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. diff --git a/lib/kfuzztest/parse.c b/lib/kfuzztest/parse.c index 3370291e9a64a7..6010171190ad48 100644 --- a/lib/kfuzztest/parse.c +++ b/lib/kfuzztest/parse.c @@ -191,16 +191,16 @@ int kfuzztest_parse_and_relocate(void *input, size_t input_size, void **arg_ret) { u32 version, magic; - if (input_size < sizeof(u64)) + if (input_size < sizeof(u32) + sizeof(u32)) return -EINVAL; - version = KFUZZTEST_GET_VERSION(*(u64 *)input); - magic = KFUZZTEST_GET_MAGIC(*(u64 *)input); + magic = *(u32 *)input; if (magic != KFUZZTEST_HEADER_MAGIC) return -EINVAL; + version = *(u32 *)((char *)input + sizeof(u32)); switch (version) { - case 0: + case KFUZZTEST_V0: return kfuzztest_parse_and_relocate_v0(input + sizeof(u64), input_size - sizeof(u64), arg_ret); }