From: Kees Cook <kees@kernel.org>
To: Vlastimil Babka <vbabka@kernel.org>
Cc: Kees Cook <kees@kernel.org>, Harry Yoo <harry@kernel.org>,
Andrew Morton <akpm@linux-foundation.org>,
Hao Li <hao.li@linux.dev>, Christoph Lameter <cl@gentwo.org>,
David Rientjes <rientjes@google.com>,
Roman Gushchin <roman.gushchin@linux.dev>,
linux-mm@kvack.org, Pedro Falcato <pfalcato@suse.de>,
Kuniyuki Iwashima <kuniyu@google.com>,
linux-hardening@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH net-next v6 6/8] mm/slab: Add tests for the existing kmem_buckets behaviour
Date: Tue, 6 Oct 2026 02:20:32 -0700 [thread overview]
Message-ID: <20261006092035.166776-6-kees@kernel.org> (raw)
In-Reply-To: <20261006092030.got.500-kees@kernel.org>
kmem_buckets has had no test coverage since it was added. Add tests,
including stuff unique to the bucket design:
- A bucket allocation comes from a cache of the set's own, and that
cache carries SLAB_NO_MERGE.
- Each size is served by a cache of the set, of the size kmalloc()
rounds it up to, including 96 and 192, which are not powers of two and
which kmalloc shares with a larger class on some configurations. Sizes
above KMALLOC_MAX_CACHE_SIZE go to the page allocator instead, bucket
set or not.
- Each cache is aligned like the kmalloc cache it mirrors.
- With CONFIG_SLAB_BUCKETS=n, kmem_buckets_create() still returns a
non-NULL (zero size alloc pointer), so that callers only have to check
for failure, and allocations through it come from the general caches.
- Destroying a set takes its caches down rather than only freeing the
set, which is what a module creating one on each load depends on.
A set freed without its caches would leave the names taken, and the
next load would warn about every one of them. This test skips when
KFENCE serves its allocation, since a KFENCE object is in none of the
cache's slabs for the teardown to find.
The tests skip rather than compile out, which is useful for testing
the CONFIG_SLAB_BUCKETS=n behaviors.
Built and tests passing (with expected skips) on ARCH=x86_64 defconfig
with GCC 16.2.0, with CONFIG_SLAB_BUCKETS as y and n.
Assisted-by: LLM
Signed-off-by: Kees Cook <kees@kernel.org>
---
lib/tests/slub_kunit.c | 228 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 228 insertions(+)
diff --git a/lib/tests/slub_kunit.c b/lib/tests/slub_kunit.c
index e3b63f0338d5..a2a15a49c5d7 100644
--- a/lib/tests/slub_kunit.c
+++ b/lib/tests/slub_kunit.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <kunit/test.h>
#include <kunit/test-bug.h>
+#include <kunit/resource.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/module.h>
@@ -9,6 +10,7 @@
#include <linux/delay.h>
#include <linux/perf_event.h>
#include <linux/kprobes.h>
+#include <linux/kfence.h>
#include "../mm/slab.h"
static struct kunit_resource resource;
@@ -474,6 +476,227 @@ static int test_init(struct kunit *test)
return 0;
}
+/* Destroy buckets on test exit so a failed KUNIT_ASSERT_*() doesn't leak. */
+KUNIT_DEFINE_ACTION_WRAPPER(destroy_buckets, kmem_buckets_destroy, kmem_buckets *);
+
+#define KUNIT_ASSERT_BUCKETS_CREATED(test, b) \
+ do { \
+ KUNIT_ASSERT_NOT_NULL(test, b); \
+ KUNIT_ASSERT_EQ(test, 0, \
+ kunit_add_action_or_reset(test, \
+ destroy_buckets, b)); \
+ } while (0)
+
+/*
+ * The cache an allocation came from, or NULL if it came from no cache at
+ * all, e.g. a size too big for any of them is served by the page allocator.
+ */
+static struct kmem_cache *cache_of(void *p)
+{
+ struct slab *slab = virt_to_slab(p);
+
+ return slab ? slab->slab_cache : NULL;
+}
+
+/*
+ * A bucket set exists to keep its allocations out of the caches everything
+ * else uses, so check the two things that make that true: they come from a
+ * cache of the set's own, and that cache is never merged into another.
+ */
+static void test_kmem_buckets_isolation(struct kunit *test)
+{
+ struct kmem_cache *bucket_cache, *general_cache;
+ kmem_buckets *b;
+ void *p, *q;
+
+ if (!IS_ENABLED(CONFIG_SLAB_BUCKETS))
+ kunit_skip(test, "needs CONFIG_SLAB_BUCKETS");
+
+ b = kmem_buckets_create("isolated_buckets", 0, INT_MAX);
+ KUNIT_ASSERT_BUCKETS_CREATED(test, b);
+
+ /*
+ * Free each allocation before asserting on the next one: the cache
+ * outlives its objects, so nothing below needs them, and an assertion
+ * that leaves one behind would make the deferred teardown report a
+ * cache that is still in use.
+ */
+ p = kmem_buckets_alloc(b, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ bucket_cache = cache_of(p);
+ kfree(p);
+ KUNIT_ASSERT_NOT_NULL(test, bucket_cache);
+
+ KUNIT_EXPECT_TRUE_MSG(test, strstarts(bucket_cache->name, "isolated_buckets-"),
+ "expected a bucket cache, got %s", bucket_cache->name);
+
+ /*
+ * Cache merging is on by default, and a bucket cache merged into a
+ * same-sized general one would quietly undo the whole separation.
+ */
+ KUNIT_EXPECT_TRUE(test, bucket_cache->flags & SLAB_NO_MERGE);
+
+ q = kmalloc(128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, q);
+ general_cache = cache_of(q);
+ kfree(q);
+ KUNIT_ASSERT_NOT_NULL(test, general_cache);
+
+ KUNIT_EXPECT_PTR_NE(test, bucket_cache, general_cache);
+}
+
+/*
+ * Each size is served by a cache of the set, of the size kmalloc() rounds it
+ * up to, including the size classes that are not powers of two, which kmalloc
+ * shares with a larger class on some configurations. Sizes past the largest
+ * cache are served by the page allocator, bucket set or not.
+ */
+static void test_kmem_buckets_sizes(struct kunit *test)
+{
+ static const size_t sizes[] = { 8, 96, 192, 1024, 4096 };
+ struct kmem_cache *c;
+ kmem_buckets *b;
+ void *p;
+ int i;
+
+ if (!IS_ENABLED(CONFIG_SLAB_BUCKETS))
+ kunit_skip(test, "needs CONFIG_SLAB_BUCKETS");
+
+ b = kmem_buckets_create("sized_buckets", 0, INT_MAX);
+ KUNIT_ASSERT_BUCKETS_CREATED(test, b);
+
+ for (i = 0; i < ARRAY_SIZE(sizes); i++) {
+ p = kmem_buckets_alloc(b, sizes[i], GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ c = cache_of(p);
+ kfree(p);
+ KUNIT_ASSERT_NOT_NULL(test, c);
+
+ KUNIT_EXPECT_TRUE_MSG(test, strstarts(c->name, "sized_buckets-"),
+ "size %zu: expected a bucket cache, got %s",
+ sizes[i], c->name);
+ KUNIT_EXPECT_EQ_MSG(test, c->object_size,
+ kmalloc_size_roundup(sizes[i]),
+ "size %zu: served by %s", sizes[i], c->name);
+ }
+
+ /* Too big for any cache: a folio from the page allocator, not a slab. */
+ p = kmem_buckets_alloc(b, KMALLOC_MAX_CACHE_SIZE + 1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ c = cache_of(p);
+ kfree(p);
+
+ KUNIT_EXPECT_NULL(test, c);
+}
+
+/*
+ * A bucket cache stands in for a kmalloc cache, so it has to be aligned like
+ * one. The DMA layer decides whether a buffer needs bouncing from its size,
+ * on the grounds that a kmalloc cache of that size is already aligned for
+ * the device, so a weaker alignment here is not something a caller can see
+ * coming. Without slab debugging the size implies the alignment and this
+ * holds either way; with it, only the cache's own alignment does.
+ */
+static void test_kmem_buckets_alignment(struct kunit *test)
+{
+ static const size_t sizes[] = { 128, 512, 2048 };
+ struct kmem_cache *bucket_cache, *general_cache;
+ kmem_buckets *b;
+ void *p;
+ int i;
+
+ if (!IS_ENABLED(CONFIG_SLAB_BUCKETS))
+ kunit_skip(test, "needs CONFIG_SLAB_BUCKETS");
+
+ b = kmem_buckets_create("aligned_buckets", 0, INT_MAX);
+ KUNIT_ASSERT_BUCKETS_CREATED(test, b);
+
+ for (i = 0; i < ARRAY_SIZE(sizes); i++) {
+ p = kmem_buckets_alloc(b, sizes[i], GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ bucket_cache = cache_of(p);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ IS_ALIGNED((unsigned long)p, ARCH_DMA_MINALIGN),
+ "size %zu: object %p is not %d byte aligned",
+ sizes[i], p, (int)ARCH_DMA_MINALIGN);
+ kfree(p);
+
+ p = kmalloc(sizes[i], GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ general_cache = cache_of(p);
+ kfree(p);
+
+ KUNIT_ASSERT_NOT_NULL(test, bucket_cache);
+ KUNIT_ASSERT_NOT_NULL(test, general_cache);
+ KUNIT_EXPECT_EQ_MSG(test, bucket_cache->align, general_cache->align,
+ "size %zu: bucket cache aligned to %u, %s to %u",
+ sizes[i], bucket_cache->align,
+ general_cache->name, general_cache->align);
+ }
+}
+
+/*
+ * With the feature compiled out, kmem_buckets_create() still returns
+ * something non-NULL so that callers only have to check for failure, and
+ * allocations through it work (i.e. come from the general caches).
+ */
+static void test_kmem_buckets_disabled(struct kunit *test)
+{
+ kmem_buckets *b;
+ struct kmem_cache *c;
+ void *p;
+
+ if (IS_ENABLED(CONFIG_SLAB_BUCKETS))
+ kunit_skip(test, "only meaningful without CONFIG_SLAB_BUCKETS");
+
+ b = kmem_buckets_create("disabled_buckets", 0, INT_MAX);
+ KUNIT_ASSERT_BUCKETS_CREATED(test, b);
+
+ p = kmem_buckets_alloc(b, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+ c = cache_of(p);
+ kfree(p);
+ KUNIT_ASSERT_NOT_NULL(test, c);
+
+ KUNIT_EXPECT_TRUE_MSG(test, !strstarts(c->name, "disabled_buckets-"),
+ "expected a general cache, got %s", c->name);
+}
+
+/* Destroying a set has to take its caches down, not just free the set. */
+static void test_kmem_buckets_destroy(struct kunit *test)
+{
+ kmem_buckets *b;
+ void *p;
+
+ if (!IS_ENABLED(CONFIG_SLAB_BUCKETS))
+ kunit_skip(test, "needs CONFIG_SLAB_BUCKETS");
+
+ b = kmem_buckets_create("destroyed_buckets", 0, INT_MAX);
+ KUNIT_ASSERT_BUCKETS_CREATED(test, b);
+
+ /*
+ * Deliberately leaked, as test_leak_destroy() leaks its own: the
+ * teardown below has to find it. kmem_cache_destroy() unlists the
+ * cache either way, so the name is still released.
+ */
+ p = kmem_buckets_alloc(b, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, p);
+
+ /*
+ * A KFENCE object is in none of the cache's slabs, so the teardown
+ * would not find it to report.
+ */
+ if (is_kfence_address(p)) {
+ kfree(p);
+ kunit_skip(test, "the allocation came from KFENCE");
+ }
+
+ /* Tear the set down now, rather than at exit, to check the report. */
+ kunit_release_action(test, destroy_buckets, b);
+
+ KUNIT_EXPECT_EQ(test, 2, slab_errors);
+}
+
static struct kunit_case test_cases[] = {
KUNIT_CASE(test_clobber_zone),
@@ -495,6 +718,11 @@ static struct kunit_case test_cases[] = {
#if defined(CONFIG_KPROBES) && defined(CONFIG_SMP)
KUNIT_CASE_SLOW(test_kmalloc_nolock_and_friends_kprobe),
#endif
+ KUNIT_CASE(test_kmem_buckets_isolation),
+ KUNIT_CASE(test_kmem_buckets_sizes),
+ KUNIT_CASE(test_kmem_buckets_alignment),
+ KUNIT_CASE(test_kmem_buckets_disabled),
+ KUNIT_CASE(test_kmem_buckets_destroy),
{}
};
--
2.55.0
next prev parent reply other threads:[~2026-10-06 9:20 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-06 9:20 [PATCH net-next v6 0/8] net: skb: isolate skb data area allocations into a separate bucket Kees Cook
2026-10-06 9:20 ` [PATCH net-next v6 1/8] mm/slab: Mark the kmem_buckets_create() context as a Context: section Kees Cook
2026-10-12 16:55 ` Vlastimil Babka (SUSE)
2026-10-06 9:20 ` [PATCH net-next v6 2/8] ipc, msg: Account msg_msg allocations with GFP_KERNEL_ACCOUNT again Kees Cook
2026-10-09 14:22 ` Harry Yoo
2026-10-09 19:39 ` Harry Yoo
2026-10-10 3:18 ` Kees Cook
2026-10-12 21:05 ` Harry Yoo
2026-10-12 21:19 ` Vlastimil Babka (SUSE)
2026-10-13 8:52 ` Kees Cook
2026-10-13 9:48 ` Vlastimil Babka (SUSE)
2026-10-10 3:14 ` Kees Cook
2026-10-10 22:37 ` Andrew Morton
2026-10-12 16:46 ` Vlastimil Babka (SUSE)
2026-10-13 3:40 ` Kees Cook
2026-10-12 17:55 ` Vlastimil Babka (SUSE)
2026-10-12 21:03 ` Vlastimil Babka (SUSE)
2026-10-13 3:46 ` Kees Cook
2026-10-06 9:20 ` [PATCH net-next v6 3/8] mm/slab: Drop the ctor and flags arguments from kmem_buckets_create() Kees Cook
2026-10-06 9:20 ` [PATCH net-next v6 4/8] mm/slab: Give bucket caches the alignment of the caches they mirror Kees Cook
2026-10-06 9:20 ` [PATCH net-next v6 5/8] mm/slab: Add kmem_buckets_destroy() Kees Cook
2026-10-06 9:20 ` Kees Cook [this message]
2026-10-06 9:20 ` [PATCH net-next v6 7/8] mm/slab: Provide kmalloc type fallback for bucket allocations Kees Cook
2026-10-06 9:20 ` [PATCH net-next v6 8/8] net: skb: isolate skb data area allocations into a separate bucket Kees Cook
2026-10-08 21:25 ` [PATCH net-next v6 0/8] " Harry Yoo
2026-10-09 6:36 ` Kees Cook
2026-10-09 7:02 ` Vlastimil Babka
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261006092035.166776-6-kees@kernel.org \
--to=kees@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=cl@gentwo.org \
--cc=hao.li@linux.dev \
--cc=harry@kernel.org \
--cc=kuniyu@google.com \
--cc=linux-hardening@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=pfalcato@suse.de \
--cc=rientjes@google.com \
--cc=roman.gushchin@linux.dev \
--cc=vbabka@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.