The slab allocator is not magic. It is a per-CPU freelist in front of a per-node partial list in front of the buddy allocator. Once you see the three layers and the five tiers between them, nothing about heap behavior surprises you.
Going through Andrey Konovalov’s (xairy) linux-kernel-exploitation for Linux and Android kernel exploit development, I came across his LSS Europe 2024 talk “SLUB Internals for Exploit Developers” — the best single reference I found for building a clear mental model of SLUB and slab shaping. Full credit to him for the structure this post follows.
This post covers SLUB allocator internals using Linux kernel 6.6 LTS as the source. All line references link back to elixir.bootlin.com for exact context. Part two covers slab shaping for OOB, UAF, double-free, and cross-cache attacks.
The Big Picture
Before diving into individual components, here is how the entire memory subsystem connects from a userspace malloc() call down to physical RAM:
┌─────────────────────────────────────────────────────┐
│ User Space │
│ malloc() / mmap() / brk() │
└─────────────────────────┬───────────────────────────┘
│ system calls
┌─────────────────────────▼───────────────────────────┐
│ Kernel │
│ │
│ kmalloc() / kmem_cache_alloc() │
│ kfree() / kmem_cache_free() │
└─────────────────────────┬───────────────────────────┘
│
┌─────────────────────────▼───────────────────────────┐
│ SLUB Allocator (mm/slub.c) │
│ │
│ Per-CPU fast path │
│ ┌──────────────────────────────────────────────┐ │
│ │ kmem_cache_cpu (one per CPU per cache) │ │
│ │ freelist → [obj] → [obj] → [obj] → NULL │ │
│ │ slab → active slab (frozen=1) │ │
│ │ partial → [slab] → [slab] → NULL │ │
│ │ tid → ABA guard (cmpxchg pair) │ │
│ └──────────────────────────────────────────────┘ │
│ │ miss (5 tiers) │
│ Per-node slow path │ │
│ ┌──────────────────────▼───────────────────────┐ │
│ │ kmem_cache_node (one per NUMA node) │ │
│ │ partial list → [slab] → [slab] → ... │ │
│ │ list_lock → spinlock │ │
│ └──────────────────────────────────────────────┘ │
│ │ miss (Tier 5) │
└─────────────────────────┼───────────────────────────┘
│ alloc_pages()
┌─────────────────────────▼───────────────────────────┐
│ Buddy Allocator (mm/page_alloc.c) │
│ manages physical page frames in 2^N page blocks │
└─────────────────────────┬───────────────────────────┘
│
┌─────────────────────────▼───────────────────────────┐
│ Physical RAM │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ page │ page │ page │ page │ page │ page │ ... │
│ └──────┴──────┴──────┴──────┴──────┴──────┘ │
│ ◄────── one slab = 1+ contiguous pages ──────► │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ obj0 │ obj1 │ obj2 │ obj3 │ obj4 │ obj5 │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┘ │
│ object slots on one slab │
└─────────────────────────────────────────────────────┘
The global struct kmem_cache (one per cache type) ties everything together: it holds the cpu_slab per-CPU pointer and the node[] per-NUMA-node pointer, along with the policy parameters (cpu_partial_slabs, min_partial, offset, random) that govern transitions between every layer.
The five allocation tiers and three freeing cases described in the rest of this post are just the rules that move slabs and objects up and down this stack.
Call Flow
Click a diagram to open it in the viewer. Links inside the SVG are clickable.
Allocation path
→ View allocation path diagram
Free path
Linux Kernel Virtual Memory Layout (x86-64)
Understanding which region an address belongs to tells you immediately what kind of object you are looking at. Using x86-64 as the reference (as in Andrey Konovalov’s LSS EU 2024 talk), the kernel virtual address space has four main regions:
| Region | What Lives Here | Examples |
|---|---|---|
| physmap | Direct mapping of all physical memory | Slab objects, page frames, kernel heap allocations |
| vmalloc | Virtually contiguous, physically scattered mappings | vmalloc buffers, ioremap, vmap’d pages |
| vmlinux | Kernel image loaded at boot | Kernel functions, global variables, read-only data, BSS |
| modules | Loadable kernel modules | Module code, module data, eBPF JIT output |
Slab objects live in the kernel linear map. The slab allocator gets pages from the buddy allocator (alloc_pages()), which guarantees physically contiguous pages. The kernel maps all physical RAM with a fixed linear offset, so every slab page is directly addressable without a page-table walk. Objects within a single slab are contiguous in both physical memory and the linear map — but two separate slabs for the same cache are not necessarily adjacent to each other.
The kernel text and modules region is separate from where slab objects live. Function pointers, vtables, and kernel symbols reside there — making it the usual target when an exploit needs to redirect execution.
Dynamic Memory Allocator Hierarchy
Linux stacks allocators from coarsest to finest granularity:
memblock -- boot-time only; reserves memory before buddy is ready
|
page_alloc -- buddy allocator; manages physical page frames in 2^N blocks
|
+-- Slab -- object allocator: carves pages into fixed-size slots
| (SLUB is the default implementation)
|
+-- vmalloc -- virtually contiguous, backed by non-contiguous physical pages
| (used when large contiguous virtual range needed but physical
| contiguity is not required)
|
+-- mempool -- pre-allocated reserve pool; guarantees allocations under
| memory pressure by falling back to its private reserve
| (backed by either Slab or page_alloc depending on type)
|
+-- percpu -- per-CPU variables and data; chunks backed by vmalloc or
| page_alloc depending on chunk type (mm/percpu.c)
|
+-- CMA -- Contiguous Memory Allocator; allocates from the MIGRATE_CMA
pool inside page_alloc; not a separate allocator but a
reservation + policy layer
The slab allocator sits directly above page_alloc. It requests groups of pages (a “slab”) from page_alloc, divides each group into equal-sized object slots, and manages a freelist of available slots. This is what kmem_cache_alloc and kmalloc ultimately return.
Slab Variants
Three implementations have existed. Only one is relevant in modern kernels:
- SLUB — default since Linux 2.6.23; Ubuntu and Android all ship with this
- SLAB — the original implementation; removed in Linux 6.8
- SLOB — tiny-embedded allocator; removed in Linux 6.4 (
CONFIG_SLUB_TINY=yreplaced it for constrained targets)
Everything below describes SLUB as implemented in 6.6.
The Slab API
Kernel code interacts with the allocator in two ways.
Named Caches (Type-Specific)
Each subsystem creates a dedicated cache for its primary object type:
// From kernel/cred.c
// https://elixir.bootlin.com/linux/v6.6/source/kernel/cred.c
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT,
NULL);
struct cred *new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
kmem_cache_free(cred_jar, new);
Well-known named caches include filp (struct file), task_struct, signal_cache, mm_struct, inode_cache, and cred_jar. All objects within one cache share the same object size, alignment, and SLAB flags.
Generic Allocations (kmalloc)
void *buf = kmalloc(142, GFP_KERNEL); // goes to kmalloc-192
kfree(buf);
kmalloc selects the smallest size class that fits the requested size. The size classes in kernel 6.6 are:
8, 16, 32, 64, 96, 128, 192, 256, 512, 1024, 2048, 4096, 8192
142 bytes does not fit in 128, so it goes to 192. Getting this wrong in an analysis means targeting the wrong cache.
The kmalloc_index() function maps a requested size to the cache index using compile-time constants for the common case. At runtime it falls through to __kmalloc, which calls kmalloc_slab() to find the right kmem_cache.
Core Data Structures
These four structures define everything the SLUB allocator tracks. Understanding them is the foundation for understanding every allocation and free operation.
List types across SLUB structures:
List Link type Why kmem_cache_cpu.freelist(via embedded freepointers)Singly linked Always consumed from the head; no mid-list removal needed kmem_cache_cpu.partial(viaslab->next)Singly linked Always consumed from the head; no mid-list removal needed kmem_cache_node.partial(viaslab->slab_list)Doubly linked ( list_head)acquire_slab()pulls slabs from arbitrary positions under the spinlock — requiresprevkmem_cache.listDoubly linked ( list_head)Caches removed from arbitrary positions in the global slab_caches list
struct kmem_cache
The global per-cache descriptor. One instance exists per named cache (and one per kmalloc size class):
// https://elixir.bootlin.com/linux/v6.6/source/include/linux/slub_def.h#L98
struct kmem_cache {
#ifndef CONFIG_SLUB_TINY
struct kmem_cache_cpu __percpu *cpu_slab;
#endif
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including metadata */
unsigned int object_size;/* The size of an object without metadata */
struct reciprocal_value reciprocal_size;
unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
/* Number of per cpu partial slabs to keep around */
unsigned int cpu_partial_slabs;
#endif
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN_GENERIC
struct kasan_cache kasan_info;
#endif
#ifdef CONFIG_HARDENED_USERCOPY
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
#endif
struct kmem_cache_node *node[MAX_NUMNODES];
}
Key fields to know:
| Field | Purpose |
|---|---|
cpu_slab | Pointer to per-CPU state (one per CPU per cache) |
object_size | The actual size requested by the subsystem |
size | object_size rounded up to alignment plus metadata |
offset | Byte offset within a free slot where the freelist pointer lives |
min_partial | Minimum slabs kmem_cache_node must retain; empty slabs are returned to buddy only when nr_partial >= min_partial, below that threshold empty slabs are kept as a reserve |
cpu_partial | Legacy tunable (/sys/kernel/slab/<cache>/cpu_partial) expressed in objects; not enforced directly. It is converted once into cpu_partial_slabs by slub_set_cpu_partial() as DIV_ROUND_UP(cpu_partial * 2, objs_per_slab), assuming slabs are half-full |
cpu_partial_slabs | Maximum slabs allowed on kmem_cache_cpu.partial, compared against slab->slabs on the list head; a free that would exceed this drains the old list to kmem_cache_node.partial |
random | Per-cache XOR secret for CONFIG_SLAB_FREELIST_HARDENED |
oo | Encoded (order, objects) pair: slab size in pages and objects per slab |
The oo field packs two values: the page order (how many physically contiguous pages form one slab) and the number of object slots that fit on a slab of that size. You can read it at runtime:
cat /sys/kernel/slab/kmalloc-256/order # page order
cat /sys/kernel/slab/kmalloc-256/objs_per_slab # slots per slab
struct kmem_cache_cpu
The per-CPU fast-path state. One instance exists for each CPU core for each cache. Allocations on the hot path only touch this struct — no locks:
// https://elixir.bootlin.com/linux/v6.6/source/include/linux/slub_def.h#L50
struct kmem_cache_cpu {
union {
struct {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
};
freelist_aba_t freelist_tid;
};
struct slab *slab; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; /* Partially allocated frozen slabs */
#endif
local_lock_t lock; /* Protects the fields above */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
The tid (transaction ID) is a monotonically increasing value used by the per-CPU fast path to prevent ABA races. In 6.6 the primitive is __update_cpu_freelist_fast(), which wraps this_cpu_try_cmpxchg_freelist() — a 128-bit compare-and-swap on the (freelist, tid) pair. (Older kernels called this this_cpu_cmpxchg_double(); the struct comment still uses that name.) When a slow-path function reloads the freelist, it also bumps the tid; the cmpxchg succeeds only if both values match exactly, catching any concurrent modification between the load and the store.
slab is the “active slab” — the slab currently providing objects for this CPU. freelist points into that active slab’s object slots, forming a lockless singly-linked list of available objects.
struct slab
One slab is one or more physically contiguous pages carved into equal-sized object slots. In Linux 5.17, struct slab was given its own type in the internal mm/slab.h header, separated from struct page at the C type level. Think of them as alias structs — they describe the same physical memory, just viewed through different types. The underlying memory layout did not change; the separation exists purely for compile-time safety. Before 5.17, slab fields lived directly inside the struct page union alongside page cache, anonymous page, and other fields — it was easy to accidentally access the wrong union member. A named struct slab type means the compiler catches that misuse at build time.
// https://elixir.bootlin.com/linux/v6.6/source/mm/slab.h#L42
/* Reuses the bits in struct page */
struct slab {
unsigned long __page_flags;
#if defined(CONFIG_SLAB)
struct kmem_cache *slab_cache;
union {
struct {
struct list_head slab_list;
void *freelist; /* array of free object indexes */
void *s_mem; /* first object */
};
struct rcu_head rcu_head;
};
unsigned int active;
#elif defined(CONFIG_SLUB)
struct kmem_cache *slab_cache;
union {
struct {
union {
struct list_head slab_list;
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct {
struct slab *next;
int slabs; /* Nr of slabs left */
};
#endif
};
/* Double-word boundary */
union {
struct {
void *freelist; /* first free object */
union {
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
#ifdef system_has_freelist_aba
freelist_aba_t freelist_counter;
#endif
};
};
struct rcu_head rcu_head;
};
unsigned int __unused;
#else
#error "Unexpected slab allocator configured"
#endif
atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
};
Fields to pay attention to:
| Field | Purpose |
|---|---|
slab_cache | Back-pointer to the kmem_cache that owns this slab |
freelist | Head of the per-slab freelist — slow path frees (cross-CPU and non-active slabs) land here; on a Tier 1 miss, Tier 2 atomically moves the entire list into the per-CPU freelist via get_freelist() |
counters | Single unsigned long that packs inuse, objects, and frozen together; allows slab_update_freelist() to atomically update all three (plus freelist) in one 128-bit cmpxchg |
inuse | Count of objects currently allocated out of this slab (bitfield inside counters) |
objects | Total object slots on this slab (bitfield inside counters) |
frozen | 1 if this slab is owned by a CPU (active or per-CPU partial); 0 if on the per-node list (bitfield inside counters) |
next / slabs | Used when slab is on a per-CPU partial list; slabs is only meaningful on the head slab and holds the total chain length |
slab_list | Used when slab is on the per-node partial list |
Note that next/slabs and slab_list share the same memory — a slab can be on a per-CPU partial list or a per-node partial list, never both simultaneously.
The frozen bit is how SLUB distinguishes active/partial slabs (owned by a CPU, frozen=1) from per-node partial slabs (frozen=0). An attempt to steal a frozen slab for another CPU is rejected.
struct kmem_cache_node
The per-NUMA-node slow path. On a single-socket system there is one node; on multi-socket NUMA systems there is one per node. All CPUs on the same NUMA node share this struct:
// https://elixir.bootlin.com/linux/v6.6/source/mm/slab.h#L776
struct kmem_cache_node {
#ifdef CONFIG_SLUB
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
The partial list holds slabs that are neither full nor active for any CPU — they have some free slots but are not currently being served from. The list_lock spinlock serializes access: any path that reads or modifies the per-node partial list must hold this lock.
nr_partial is a simple count of slabs on the list. If nr_partial < min_partial, SLUB will not release empty slabs from this list back to page_alloc — they are kept as a reserve to avoid repeated round-trips to the buddy allocator. Once nr_partial >= min_partial, an emptied slab is discarded.
Slab States
At any moment a slab is in exactly one of these states:
kmem_cache
├─ cpu_slab[n] (one per CPU)
│ ├─ ->slab active slab frozen=1
│ └─ ->partial per-CPU partial frozen=1
└─ node[n] (one per NUMA node)
└─ ->partial per-node partial frozen=0
[full slabs: frozen=0, not on any list, not tracked]
| State | Where Referenced | frozen | Description |
|---|---|---|---|
| Active | cpu_slab->slab | 1 | One per CPU; all allocations start here |
| Per-CPU partial | cpu_slab->partial (list via slab->next) | 1 | Partially free; reserved for this CPU — frozen=1 because it is still held by the CPU, not yet returned to the node |
| Per-node partial | node->partial (list via slab->slab_list) | 0 | Partially free; shared across CPUs |
| Full | (not tracked) | 0 | No free slots; not on any list; SLUB only tracks these under slub_debug |
Full slabs are invisible to normal SLUB operation: since there is nothing to allocate from them, they need not be tracked. Both full slabs and per-node partial slabs have frozen=0 — the difference is that full slabs are on no list at all, while per-node partial slabs are on node->partial. A slab transitions from full to per-CPU partial when any object on it is freed.
Two Freelists on the Active Slab
This is the most commonly misunderstood aspect of SLUB. The active slab has two independent freelists:
Active slab (slab->frozen = 1)
|
+-- kmem_cache_cpu->freelist (per-CPU lockless freelist)
| Used for: alloc and free by THIS CPU on the active slab
| Access: lockless, compare-and-swap with tid
|
+-- slab->freelist (per-slab freelist)
Used for: frees by OTHER CPUs targeting this slab
Access: cmpxchg on (freelist, counters) pair
Per-CPU lockless freelist (cpu_slab->freelist):
This is the hot path. Every allocation on the current CPU pops the head of this list. Every free of an object in the active slab pushes to the head of this list. No lock is taken — the freelist head and tid are updated with __update_cpu_freelist_fast(), which atomically swaps two adjacent unsigned long values as a single 128-bit operation.
Per-slab freelist (slab->freelist):
When another CPU frees an object that lives in this CPU’s active slab, it cannot safely modify cpu_slab->freelist (which belongs to the other CPU). Instead it atomically updates slab->freelist using slab_update_freelist(), which swaps the (freelist, counters) pair embedded in the slab descriptor with a 128-bit cmpxchg (or, on hardware without one, under the slab_lock() bit-spinlock on the page).
Naming note: kernels before 6.5 called these
this_cpu_cmpxchg_double()andcmpxchg_double_slab(). Many talks and writeups still use those names; in 6.6 the functions are__update_cpu_freelist_fast()andslab_update_freelist()/__slab_update_freelist(). Same semantics.
Merging the two freelists:
When cpu_slab->freelist runs dry, the allocator checks slab->freelist. If it is non-empty, the slow-path function get_freelist() atomically claims slab->freelist and moves it into cpu_slab->freelist:
After get_freelist succeeds, slab->freelist is NULL (atomically set to NULL) and the returned pointer becomes the new cpu_slab->freelist. The slab’s inuse counter is set to objects (all slots in use from the slab’s perspective) because the lockless per-CPU freelist now holds the free slots — they are logically “owned” by the CPU, not the slab.
The line new.frozen = freelist != NULL encodes a subtle invariant: if slab->freelist was non-empty, the slab stays frozen because the returned objects are now on cpu_slab->freelist — the CPU still owns it. If slab->freelist was already empty, frozen becomes 0 and ___slab_alloc() immediately drops it from the active position (c->slab = NULL, L3155) in the same call, then moves on to Tier 3. The now-full slab is on no list and is unfrozen; it re-enters the system only when one of its objects is freed.
Freelist Pointer Layout and Hardening
Pointer Placement
The freelist next-pointer stored inside a free slot is not at offset 0. It is placed near the middle of the object — at ALIGN_DOWN(object_size / 2, sizeof(void *)) — see calculate_sizes() (the offset assignment itself is at L4439).
So for a kmalloc-256 object (size = 256 bytes), the freelist pointer lives at byte offset 128. For kmalloc-128 it is at offset 64. This placement makes it harder for a small linear overflow from one slot’s end to reach the freelist pointer in the adjacent free slot without crossing many bytes first.
Caches with SLAB_TYPESAFE_BY_RCU, SLAB_POISON, a constructor (ctor), or slub_debug original-size tracking place the pointer after the object instead (s->offset = size), because the first word of the object must not be overwritten on free.
CONFIG_SLAB_FREELIST_HARDENED
When enabled (default on Ubuntu, Fedora, Android production kernels), the freelist pointer stored in a slot is not the raw address of the next free object. It is encoded:
→ freelist_ptr_decode()
→ freelist_ptr_encode()
The encoding formula uses three components:
- The actual pointer value (
ptr) - A per-cache boot-time random secret (
s->random) - The address of the memory location that holds this pointer (
ptr_addr), byte-swapped withswab()
swab() reverses the byte order of the full unsigned long — on a 64-bit system, byte 0 swaps with byte 7, byte 1 with byte 6, and so on. Combined with the per-cache secret, this makes it infeasible to forge a valid encoded pointer without knowing s->random, even if you know where you want the freelist to point. The scheme is only as strong as the secret: one leaked encoded pointer whose plaintext value and storage address are both known yields s->random = encoded ^ ptr ^ swab(ptr_addr) directly.
s->random is initialized at cache creation via get_random_long() in kmem_cache_open().
CONFIG_SLAB_FREELIST_RANDOM
When a brand new slab is allocated from page_alloc, SLUB shuffles the order of its object slots before use via shuffle_freelist(), called from allocate_slab().
Without this shuffle, a fresh slab has objects laid out sequentially: slot 0, slot 1, slot 2, … With the shuffle, the allocation order is randomized. Two consecutive kmalloc calls from a fresh slab will not give you sequentially adjacent slots.
Existing partial slabs and slabs whose freelist was manipulated by frees are not reshuffled — randomization only applies at slab-creation time.
5-Tier Allocation Path
When kmem_cache_alloc() or kmalloc() is called, SLUB walks through five tiers in strict order, stopping at the first tier that succeeds.
The entry point is slab_alloc_node() which calls __slab_alloc_node().
Tier 1 — Per-CPU Lockless Freelist
The fast path. If c->freelist is non-NULL, SLUB pops the head:
- Read
c->freelist(head) andc->tid - Read the next pointer from inside the head object (the encoded freepointer)
- Atomically swap
(freelist=head, tid=old_tid)with(freelist=next, tid=new_tid)via__update_cpu_freelist_fast() - If the cmpxchg succeeds, return
head. If it fails (another path modified the CPU slab between steps 1 and 3), retry from the start
This path takes no locks and performs one atomic operation.
Tier 2 — Active Slab Freelist Merge
When c->freelist is empty, the slow path (__slab_alloc) checks slab->freelist. If that is non-empty, get_freelist() atomically moves the entire per-slab freelist into c->freelist, then allocates from the now-populated per-CPU freelist.
The key transition: slab->freelist becomes NULL and slab->inuse is set to slab->objects. The free slots now live in the per-CPU freelist, not in the slab descriptor.
Tier 3 — Per-CPU Partial Promotion
If both freelists on the active slab are empty (the slab is full), SLUB checks c->partial. If it is non-empty, the first slab on the per-CPU partial list is promoted to active — see the slub_percpu_partial(c) branch in ___slab_alloc().
After promotion, execution jumps to Tier 2 (merge the new active slab’s slab->freelist).
Tier 4 — Per-Node Partial List
If the per-CPU partial list is also empty, SLUB acquires the per-node list_lock and pulls slabs from node->partial:
Two things happen here:
- The first slab on the node partial list is designated as the new active slab
- Additional slabs are moved to the per-CPU partial list via
put_cpu_partial(s, slab, 0)until the count exceedscpu_partial_slabs / 2(L2319) — so up tocpu_partial_slabs / 2 + 1extra slabs
This batch pull is an optimization to avoid returning to the per-node list immediately on the next allocation. Note the drain=0 argument: the batch pull never triggers a drain of the per-CPU partial list, even if it pushes the count past cpu_partial_slabs. Only frees (drain=1, see Case 3 below) can drain.
Tier 5 — New Slab from Page Allocator
If the per-node partial list is also empty, a brand new slab is allocated from page_alloc via allocate_slab(). It first tries the preferred page order (s->oo). If that fails under memory pressure, it falls back to s->min (a smaller slab). The freelist is built by linking all slots sequentially, or shuffled if CONFIG_SLAB_FREELIST_RANDOM is enabled.
3-Case Freeing Path
When kfree() or kmem_cache_free() is called, the freeing path in do_slab_free() dispatches to one of three cases based on where the freed object lives relative to the current CPU’s active slab.
The entry point:
Case 1 — Active Slab, Current CPU (Fast Path)
The freed object lives in c->slab (the current CPU’s active slab). The fast path pushes it to the head of c->freelist atomically via __update_cpu_freelist_fast():
Before: c->freelist -> [A] -> [B] -> NULL
Free(victim), victim in c->slab:
After: c->freelist -> [victim] -> [A] -> [B] -> NULL
This is a LIFO (last in, first out) insertion. The next allocation on this CPU will return victim.
Case 2 — Non-Active Slab (Slow Path via __slab_free)
The freed object lives in a slab that is not the current CPU’s active slab. This covers:
- Another CPU’s active slab
- Any per-CPU partial slab (this CPU or another)
- Any per-node partial slab
The slow path is __slab_free(). The free is performed by atomically prepending to slab->freelist via slab_update_freelist(), which atomically exchanges (slab->freelist, slab->counters). The loop retries until the cmpxchg succeeds.
What happens next depends on was_frozen (the frozen bit read before the cmpxchg) and prior (the old slab->freelist):
was_frozen == 1(another CPU’s active slab, or any per-CPU partial slab): nothing else. The object sits onslab->freelistuntil the owning CPU merges it (Tier 2) or drains the slab. Even ifinusedrops to 0, the slab is not returned topage_allochere — an empty per-CPU partial slab stays put until__unfreeze_partials()runs.was_frozen == 0andprior == NULL(the slab was full and on no list): it transitions from full to per-CPU partial — see Case 3 below. Note the code tests!prior(freelist was empty), notinuse == objects; for an unfrozen slab the two are equivalent, but!prioris also true for a frozen active slab whose freelist was just taken byget_freelist(), which is whywas_frozenis checked first.was_frozen == 0andprior != NULL(on the per-node partial list): ifnew.inusedrops to 0 andn->nr_partial >= min_partial, the slab is removed from the list and returned topage_alloc(discard_slab). Otherwise it stays on the node partial list.
Case 3 — Freeing from a Full Slab
A full slab has slab->freelist == NULL and slab->inuse == slab->objects. When an object in a full slab is freed:
Step 1 — the object is freed into slab->freelist via the same slab_update_freelist() loop as Case 2. Because !prior && !was_frozen, the same cmpxchg also sets new.frozen = 1 — the freeing CPU claims the slab in the same atomic step.
Step 2 — the slab is now frozen but on no list, so SLUB calls put_cpu_partial(s, slab, 1) to hang it on the freeing CPU’s partial list:
Two subcases:
Subcase A — partial list has room (current count < cpu_partial_slabs): the newly-partial slab is prepended to c->partial. slab->next points to the former head. slab->slabs is set to the old head’s count plus one — the total chain length including itself.
Subcase B — partial list is full (count >= cpu_partial_slabs): the old list is displaced and the newly-partial slab is prepended to c->partial inside the lock (L2726). After releasing the lock (L2728), the displaced old list is drained via __unfreeze_partials() outside the lock (L2731). Each slab in the old list has its frozen bit cleared and is moved to node->partial; empty slabs are freed to page_alloc if node->nr_partial >= min_partial.
Drain Mechanics
The drain that happens in Case 3 Subcase B is __unfreeze_partials(). Each slab being drained has its frozen bit cleared to 0 atomically (via __slab_update_freelist()). Then:
- If the slab is fully empty and
node->nr_partial >= min_partial: the slab is discarded (pages returned topage_alloc) - Otherwise: the slab is added to the tail of
node->partial
This is the slab lifecycle event that triggers cross-cache interactions: when an empty slab is returned to page_alloc, those pages become available to any cache that requests a new slab next.
The Tier 4 batch pull (get_partial_node) is the inverse: it moves roughly cpu_partial_slabs / 2 slabs from the node list to the per-CPU partial list when the per-CPU list is depleted.
Diving into the Source
The diagrams cover the common path. For edge cases and implementation details,
start from these two functions in mm/slub.c:
__slab_alloc_node()— allocation entry pointdo_slab_free()— freeing entry point
References
- Andrey Konovalov (xairy) — SLUB Internals for Exploit Developers, LSS Europe 2024. The primary reference for the structure of this post.
- linux-kernel-exploitation — github.com/xairy/linux-kernel-exploitation — curated papers, CVE writeups, and workshops.
- Linux kernel 6.6 source — elixir.bootlin.com/linux/v6.6/source — the canonical reference for all implementation details above.
- Linux Kernel Internals — https://kernel-internals.org/mm/overview/ — Memory Management Internals