[RFC PATCH 3/3] mm/slub: add KUnit coverage for the kvmalloc order limit
Daniil Tatianin <[email protected]> Wed, 5 Aug 2026 12:48:43 +0300
| Newsgroups | gmane.linux.kernel,gmane.linux.documentation,gmane.linux.kernel.mm |
|---|---|
| Message-ID | <[email protected]> |
The limit has no observable effect other than which allocator served a request, so it is easy to break without noticing. Add a KUnit suite that checks the behavior directly. is_vmalloc_addr() is what the tests key on: a request the limit denied must have come from vmalloc(). The reverse is deliberately not asserted, as kmalloc() may always fail and fall back on its own, which would make such a test depend on how fragmented the machine happens to be. Covered: - a request one byte above the limit is served by vmalloc(), - a request of exactly the limit is not denied, - requests a kmalloc cache can serve are not denied even with the limit at the lowest value the sysctl accepts, which is the invariant that lets kvmalloc_order_denied() get away with a single order comparison, - at the default value nothing is denied, - requests above KMALLOC_MAX_SIZE are not accounted to the limit, - sub page requests are never diverted. The suite drives sysctl_kvmalloc_max_contig_order directly instead of going through the sysctl, so the variable is made visible to the test with VISIBLE_IF_KUNIT and EXPORT_SYMBOL_IF_KUNIT and declared in mm/slab.h. It stays static, and unexported, when CONFIG_KUNIT is disabled. Signed-off-by: Daniil Tatianin <[email protected]> --- MAINTAINERS | 1 + lib/Kconfig.debug | 15 +++ lib/tests/Makefile | 1 + lib/tests/kvmalloc_kunit.c | 184 +++++++++++++++++++++++++++++++++++++ mm/slab.h | 4 + mm/slub.c | 4 +- 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 lib/tests/kvmalloc_kunit.c diff --git a/MAINTAINERS b/MAINTAINERS index 716acfc3d7c1..128dd3508e66 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -24915,6 +24915,7 @@ F: Documentation/admin-guide/mm/slab.rst F: Documentation/mm/slab.rst F: include/linux/mempool.h F: include/linux/slab.h +F: lib/tests/kvmalloc_kunit.c F: lib/tests/slub_kunit.c F: mm/failslab.c F: mm/mempool.c diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 1244dcac2294..3025d5693891 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -2998,6 +2998,21 @@ config SLUB_KUNIT_TEST If unsure, say N. +config KVMALLOC_ORDER_LIMIT_KUNIT_TEST + tristate "KUnit test for the kvmalloc order limit" if !KUNIT_ALL_TESTS + depends on KVMALLOC_ORDER_LIMIT && KUNIT + default KUNIT_ALL_TESTS + help + This builds the unit test for vm.kvmalloc_max_contig_order. + Tests that requests above the limit are served by vmalloc(), that + requests a kmalloc cache can serve are never diverted, and that the + kvmalloc_forced_vmalloc counter only accounts allocations the limit + actually diverted. + For more information on KUnit and unit tests in general please refer + to the KUnit documentation in Documentation/dev-tools/kunit/. + + If unsure, say N. + config RATIONAL_KUNIT_TEST tristate "KUnit test for rational.c" if !KUNIT_ALL_TESTS depends on KUNIT && RATIONAL diff --git a/lib/tests/Makefile b/lib/tests/Makefile index 4ead57602eac..19ecc339b227 100644 --- a/lib/tests/Makefile +++ b/lib/tests/Makefile @@ -48,6 +48,7 @@ obj-$(CONFIG_RANDSTRUCT_KUNIT_TEST) += randstruct_kunit.o obj-$(CONFIG_SCANF_KUNIT_TEST) += scanf_kunit.o obj-$(CONFIG_SEQ_BUF_KUNIT_TEST) += seq_buf_kunit.o obj-$(CONFIG_SIPHASH_KUNIT_TEST) += siphash_kunit.o +obj-$(CONFIG_KVMALLOC_ORDER_LIMIT_KUNIT_TEST) += kvmalloc_kunit.o obj-$(CONFIG_SLUB_KUNIT_TEST) += slub_kunit.o obj-$(CONFIG_TEST_SORT) += test_sort.o CFLAGS_stackinit_kunit.o += $(call cc-disable-warning, switch-unreachable) diff --git a/lib/tests/kvmalloc_kunit.c b/lib/tests/kvmalloc_kunit.c new file mode 100644 index 000000000000..3c792fddc63e --- /dev/null +++ b/lib/tests/kvmalloc_kunit.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KUnit tests for the vm.kvmalloc_max_contig_order limit. + * + * The observable the tests rely on is is_vmalloc_addr(): a request the limit + * denied must have been served by vmalloc(), never by kmalloc(). The reverse + * direction is deliberately not asserted, as kmalloc() is always free to fail + * and fall back to vmalloc() on its own, which would make such a test depend + * on how fragmented the machine happens to be. + */ +#include <kunit/test.h> +#include <kunit/visibility.h> +#include <linux/mm.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/vmalloc.h> +#include <linux/vmstat.h> +#include "../mm/slab.h" + +/* The order used whenever a test needs a limit below MAX_PAGE_ORDER. */ +#define TEST_ORDER PAGE_ALLOC_COSTLY_ORDER +/* The smallest limit the sysctl accepts, i.e. the order of a full kmalloc cache. */ +#define TEST_ORDER_MIN (KMALLOC_SHIFT_HIGH - PAGE_SHIFT) + +static unsigned int saved_order; + +static unsigned long forced_vmalloc_count(struct kunit *test) +{ + unsigned long *events, count; + + events = kunit_kcalloc(test, NR_VM_EVENT_ITEMS, sizeof(*events), + GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, events); + + all_vm_events(events); + count = events[KVMALLOC_FORCED_VMALLOC]; + + kunit_kfree(test, events); + return count; +} + +/* + * Allocate @size with the limit set to @order and report whether the result + * came from vmalloc(), along with how much the counter moved. + */ +static bool alloc_at_order(struct kunit *test, size_t size, unsigned int order, + unsigned long *counted) +{ + unsigned long before, after; + bool vmalloced; + void *p; + + sysctl_kvmalloc_max_contig_order = order; + + before = forced_vmalloc_count(test); + p = kvmalloc(size, GFP_KERNEL); + after = forced_vmalloc_count(test); + + sysctl_kvmalloc_max_contig_order = MAX_PAGE_ORDER; + + KUNIT_ASSERT_NOT_NULL(test, p); + vmalloced = is_vmalloc_addr(p); + kvfree(p); + + if (counted) + *counted = after - before; + + return vmalloced; +} + +/* A request above the limit must not be served by kmalloc(). */ +static void test_over_limit_is_vmalloc(struct kunit *test) +{ + size_t size = (PAGE_SIZE << TEST_ORDER) + 1; + unsigned long nr; + + KUNIT_EXPECT_TRUE(test, alloc_at_order(test, size, TEST_ORDER, &nr)); + KUNIT_EXPECT_EQ(test, nr, 1UL); +} + +/* A request of exactly the limit is still allowed to use kmalloc(). */ +static void test_at_limit_is_not_denied(struct kunit *test) +{ + size_t size = PAGE_SIZE << TEST_ORDER; + unsigned long nr; + + alloc_at_order(test, size, TEST_ORDER, &nr); + KUNIT_EXPECT_EQ(test, nr, 0UL); +} + +/* + * Requests a kmalloc cache can serve are never denied, even with the limit at + * the smallest value the sysctl accepts. + */ +static void test_cache_sized_never_denied(struct kunit *test) +{ + size_t size = KMALLOC_MAX_CACHE_SIZE; + unsigned long nr; + + KUNIT_EXPECT_FALSE(test, alloc_at_order(test, size, TEST_ORDER_MIN, &nr)); + KUNIT_EXPECT_EQ(test, nr, 0UL); + + size = PAGE_SIZE; + KUNIT_EXPECT_FALSE(test, alloc_at_order(test, size, TEST_ORDER_MIN, &nr)); + KUNIT_EXPECT_EQ(test, nr, 0UL); +} + +/* At the default the limit must be inert, whichever way the allocation goes. */ +static void test_default_does_not_deny(struct kunit *test) +{ + size_t size = PAGE_SIZE << TEST_ORDER; + unsigned long nr; + + alloc_at_order(test, size, MAX_PAGE_ORDER, &nr); + KUNIT_EXPECT_EQ(test, nr, 0UL); + + size = KMALLOC_MAX_SIZE; + alloc_at_order(test, size, MAX_PAGE_ORDER, &nr); + KUNIT_EXPECT_EQ(test, nr, 0UL); +} + +/* + * kmalloc() cannot serve anything above KMALLOC_MAX_SIZE, so such a request + * reaches vmalloc() either way and must not be accounted to the limit. + */ +static void test_over_kmalloc_max_not_counted(struct kunit *test) +{ + size_t size = KMALLOC_MAX_SIZE + PAGE_SIZE; + unsigned long nr; + + KUNIT_EXPECT_TRUE(test, alloc_at_order(test, size, TEST_ORDER, &nr)); + KUNIT_EXPECT_EQ(test, nr, 0UL); +} + +/* Sub-page requests never fall back to vmalloc(), limit or not. */ +static void test_sub_page_untouched(struct kunit *test) +{ + unsigned long nr; + + KUNIT_EXPECT_FALSE(test, alloc_at_order(test, 64, TEST_ORDER_MIN, &nr)); + KUNIT_EXPECT_EQ(test, nr, 0UL); +} + +static int kvmalloc_limit_init(struct kunit *test) +{ + /* + * TEST_ORDER has to be a value the sysctl would accept, otherwise the + * tests would be exercising a state userspace cannot reach. + */ + if (TEST_ORDER < TEST_ORDER_MIN || TEST_ORDER >= MAX_PAGE_ORDER) + kunit_skip(test, "TEST_ORDER %d outside the accepted range [%d, %d]", + TEST_ORDER, TEST_ORDER_MIN, MAX_PAGE_ORDER); + + saved_order = sysctl_kvmalloc_max_contig_order; + return 0; +} + +static void kvmalloc_limit_exit(struct kunit *test) +{ + sysctl_kvmalloc_max_contig_order = saved_order; +} + +static struct kunit_case kvmalloc_limit_cases[] = { + KUNIT_CASE(test_over_limit_is_vmalloc), + KUNIT_CASE(test_at_limit_is_not_denied), + KUNIT_CASE(test_cache_sized_never_denied), + KUNIT_CASE(test_default_does_not_deny), + KUNIT_CASE(test_over_kmalloc_max_not_counted), + KUNIT_CASE(test_sub_page_untouched), + {} +}; + +static struct kunit_suite kvmalloc_limit_suite = { + .name = "kvmalloc_order_limit", + .init = kvmalloc_limit_init, + .exit = kvmalloc_limit_exit, + .test_cases = kvmalloc_limit_cases, +}; + +kunit_test_suite(kvmalloc_limit_suite); + +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); +MODULE_DESCRIPTION("KUnit tests for the kvmalloc order limit"); +MODULE_LICENSE("GPL"); diff --git a/mm/slab.h b/mm/slab.h index f5e336b6b6b0..157845af6e45 100644 --- a/mm/slab.h +++ b/mm/slab.h @@ -782,4 +782,8 @@ static inline bool slub_debug_orig_size(struct kmem_cache *s) void skip_orig_size_check(struct kmem_cache *s, const void *object); #endif +#if defined(CONFIG_KVMALLOC_ORDER_LIMIT) && IS_ENABLED(CONFIG_KUNIT) +extern unsigned int sysctl_kvmalloc_max_contig_order; +#endif + #endif /* MM_SLAB_H */ diff --git a/mm/slub.c b/mm/slub.c index 7287949ea247..2502bc74f506 100644 --- a/mm/slub.c +++ b/mm/slub.c @@ -52,6 +52,7 @@ #include <linux/debugfs.h> #include <linux/sysctl.h> #include <linux/vmstat.h> +#include <kunit/visibility.h> #include <trace/events/kmem.h> #include "internal.h" @@ -6890,7 +6891,8 @@ static gfp_t kmalloc_gfp_adjust(gfp_t flags, size_t size) } #ifdef CONFIG_KVMALLOC_ORDER_LIMIT -static unsigned int sysctl_kvmalloc_max_contig_order __read_mostly = MAX_PAGE_ORDER; +VISIBLE_IF_KUNIT unsigned int sysctl_kvmalloc_max_contig_order __read_mostly = MAX_PAGE_ORDER; +EXPORT_SYMBOL_IF_KUNIT(sysctl_kvmalloc_max_contig_order); /* * The limit only ever applies to requests too large for a kmalloc cache, so