[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