[PATCH 1/8] mm/slub: Support disabling freelist hardening at boot

From: Chris Down

Date: Tue Oct 27 2026 - 03:37:46 EST


Add the slab_freelist_hardened= early parameter, enabled by default on
kernels built with CONFIG_SLAB_FREELIST_HARDENED=y.

Signed-off-by: Chris Down <chris@xxxxxxxxxxxxxx>
---
.../admin-guide/kernel-parameters.txt | 7 +++
mm/Kconfig | 3 ++
mm/slab.h | 13 +++++
mm/slub.c | 49 +++++++++++++++----
scripts/gdb/linux/slab.py | 8 ++-
5 files changed, 69 insertions(+), 11 deletions(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 68647ff4bdd2..ddf2f6fd60be 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -7014,6 +7014,13 @@ Kernel parameters
option which can be undone by adding the
"hash_pointers=always" option.

+ slab_freelist_hardened= [MM,EARLY]
+ Format: <bool>
+ Enable or disable slab freelist hardening on kernels
+ built with CONFIG_SLAB_FREELIST_HARDENED=y. Ignored
+ without that option.
+ Default: on.
+
slab_max_order= [MM]
Determines the maximum allowed order for slabs.
A high setting may cause OOMs due to memory
diff --git a/mm/Kconfig b/mm/Kconfig
index 604c58199acb..3ae97fec9946 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -214,6 +214,9 @@ config SLAB_FREELIST_HARDENED
sacrifices to harden the kernel slab allocator against common
freelist exploit methods.

+ The hardening is enabled by default and can be disabled at boot
+ with "slab_freelist_hardened=0".
+
config SLAB_BUCKETS
bool "Support allocation from separate kmalloc buckets"
depends on !SLUB_TINY
diff --git a/mm/slab.h b/mm/slab.h
index e94ac63d10dc..8187ddb2face 100644
--- a/mm/slab.h
+++ b/mm/slab.h
@@ -540,6 +540,19 @@ static inline bool kmem_cache_debug_flags(struct kmem_cache *s, slab_flags_t fla
return false;
}

+#ifdef CONFIG_SLAB_FREELIST_HARDENED
+DECLARE_STATIC_KEY_TRUE(slab_freelist_hardened);
+#endif
+
+static inline bool slab_freelist_hardened_enabled(void)
+{
+#ifdef CONFIG_SLAB_FREELIST_HARDENED
+ return static_branch_likely(&slab_freelist_hardened);
+#else
+ return false;
+#endif
+}
+
#if IS_ENABLED(CONFIG_SLUB_DEBUG) && IS_ENABLED(CONFIG_KUNIT)
bool slab_in_kunit_test(void);
#else
diff --git a/mm/slub.c b/mm/slub.c
index 8fbc6bf8ebc5..c82bc626b657 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -214,6 +214,32 @@ DEFINE_STATIC_KEY_FALSE(slub_debug_enabled);
static DEFINE_STATIC_KEY_FALSE(strict_numa);
#endif

+#ifdef CONFIG_SLAB_FREELIST_HARDENED
+/*
+ * Free pointers stored inside free objects are encoded according to this key,
+ * so it must not change once the first slab has been populated.
+ */
+DEFINE_STATIC_KEY_TRUE_RO(slab_freelist_hardened);
+static bool slab_freelist_hardened_early __initdata = true;
+
+static int __init setup_slab_freelist_hardened(char *str)
+{
+ return kstrtobool(str, &slab_freelist_hardened_early);
+}
+early_param("slab_freelist_hardened", setup_slab_freelist_hardened);
+
+static void __init init_freelist_hardening(void)
+{
+ if (slab_freelist_hardened_early)
+ return;
+
+ static_branch_disable(&slab_freelist_hardened);
+ pr_info("SLUB: freelist hardening disabled\n");
+}
+#else
+static inline void init_freelist_hardening(void) { }
+#endif
+
#ifdef CONFIG_MEM_ALLOC_PROFILING
DEFINE_STATIC_KEY_MAYBE(CONFIG_MEM_ALLOC_PROFILING_ENABLED_BY_DEFAULT,
slab_obj_ext_has_codetag_key);
@@ -565,10 +591,10 @@ static inline freeptr_t freelist_ptr_encode(const struct kmem_cache *s,
{
unsigned long encoded;

-#ifdef CONFIG_SLAB_FREELIST_HARDENED
- encoded = (unsigned long)ptr ^ s->random ^ swab(ptr_addr);
-#else
encoded = (unsigned long)ptr;
+#ifdef CONFIG_SLAB_FREELIST_HARDENED
+ if (slab_freelist_hardened_enabled())
+ encoded ^= s->random ^ swab(ptr_addr);
#endif
return (freeptr_t){.v = encoded};
}
@@ -578,10 +604,10 @@ static inline void *freelist_ptr_decode(const struct kmem_cache *s,
{
void *decoded;

-#ifdef CONFIG_SLAB_FREELIST_HARDENED
- decoded = (void *)(ptr.v ^ s->random ^ swab(ptr_addr));
-#else
decoded = (void *)ptr.v;
+#ifdef CONFIG_SLAB_FREELIST_HARDENED
+ if (slab_freelist_hardened_enabled())
+ decoded = (void *)(ptr.v ^ s->random ^ swab(ptr_addr));
#endif
return decoded;
}
@@ -601,9 +627,9 @@ static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;

-#ifdef CONFIG_SLAB_FREELIST_HARDENED
- BUG_ON(object == fp); /* naive detection of double free or corruption */
-#endif
+ /* naive detection of double free or corruption */
+ if (slab_freelist_hardened_enabled())
+ BUG_ON(object == fp);

freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
*(freeptr_t *)freeptr_addr = freelist_ptr_encode(s, fp, freeptr_addr);
@@ -6730,7 +6756,7 @@ void kmem_cache_free(struct kmem_cache *s, void *x)

slab = virt_to_slab(x);

- if (IS_ENABLED(CONFIG_SLAB_FREELIST_HARDENED) ||
+ if (slab_freelist_hardened_enabled() ||
kmem_cache_debug_flags(s, SLAB_CONSISTENCY_CHECKS)) {

/*
@@ -8766,6 +8792,9 @@ void __init kmem_cache_init(void)
if (debug_guardpage_minorder())
slub_max_order = 0;

+ /* Must be settled before any free pointer is written. */
+ init_freelist_hardening();
+
/* Inform pointer hashing choice about slub debugging state. */
hash_pointers_finalize(__slub_debug_enabled());

diff --git a/scripts/gdb/linux/slab.py b/scripts/gdb/linux/slab.py
index ddde25aeca8d..eab6cf44b54b 100644
--- a/scripts/gdb/linux/slab.py
+++ b/scripts/gdb/linux/slab.py
@@ -96,8 +96,14 @@ def swab64(x):
((x & 0xff00000000000000) >> 56))
return result

+def freelist_hardened():
+ if not constants.LX_CONFIG_SLAB_FREELIST_HARDENED:
+ return False
+ key = gdb.parse_and_eval("slab_freelist_hardened")
+ return int(key['key']['enabled']['counter']) > 0
+
def freelist_ptr_decode(cache, ptr, ptr_addr):
- if constants.LX_CONFIG_SLAB_FREELIST_HARDENED:
+ if freelist_hardened():
return ptr['v'] ^ cache['random'] ^ swab64(int(ptr_addr))
else:
return ptr['v']
--
2.54.0