Skip to content
Go back

SLUB Allocator Internals

Published:  at  07:00 PM

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

View free path diagram


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:

RegionWhat Lives HereExamples
physmapDirect mapping of all physical memorySlab objects, page frames, kernel heap allocations
vmallocVirtually contiguous, physically scattered mappingsvmalloc buffers, ioremap, vmap’d pages
vmlinuxKernel image loaded at bootKernel functions, global variables, read-only data, BSS
modulesLoadable kernel modulesModule 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:

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.

kmalloc()

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:

ListLink typeWhy
kmem_cache_cpu.freelist (via embedded freepointers)Singly linkedAlways consumed from the head; no mid-list removal needed
kmem_cache_cpu.partial (via slab->next)Singly linkedAlways consumed from the head; no mid-list removal needed
kmem_cache_node.partial (via slab->slab_list)Doubly linked (list_head)acquire_slab() pulls slabs from arbitrary positions under the spinlock — requires prev
kmem_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:

FieldPurpose
cpu_slabPointer to per-CPU state (one per CPU per cache)
object_sizeThe actual size requested by the subsystem
sizeobject_size rounded up to alignment plus metadata
offsetByte offset within a free slot where the freelist pointer lives
min_partialMinimum 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_partialLegacy 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_slabsMaximum 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
randomPer-cache XOR secret for CONFIG_SLAB_FREELIST_HARDENED
ooEncoded (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:

FieldPurpose
slab_cacheBack-pointer to the kmem_cache that owns this slab
freelistHead 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()
countersSingle 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
inuseCount of objects currently allocated out of this slab (bitfield inside counters)
objectsTotal object slots on this slab (bitfield inside counters)
frozen1 if this slab is owned by a CPU (active or per-CPU partial); 0 if on the per-node list (bitfield inside counters)
next / slabsUsed when slab is on a per-CPU partial list; slabs is only meaningful on the head slab and holds the total chain length
slab_listUsed 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]
StateWhere ReferencedfrozenDescription
Activecpu_slab->slab1One per CPU; all allocations start here
Per-CPU partialcpu_slab->partial (list via slab->next)1Partially free; reserved for this CPU — frozen=1 because it is still held by the CPU, not yet returned to the node
Per-node partialnode->partial (list via slab->slab_list)0Partially free; shared across CPUs
Full(not tracked)0No 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() and cmpxchg_double_slab(). Many talks and writeups still use those names; in 6.6 the functions are __update_cpu_freelist_fast() and slab_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:

get_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:

  1. The actual pointer value (ptr)
  2. A per-cache boot-time random secret (s->random)
  3. The address of the memory location that holds this pointer (ptr_addr), byte-swapped with swab()

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:

  1. Read c->freelist (head) and c->tid
  2. Read the next pointer from inside the head object (the encoded freepointer)
  3. Atomically swap (freelist=head, tid=old_tid) with (freelist=next, tid=new_tid) via __update_cpu_freelist_fast()
  4. 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:

get_partial_node()

Two things happen here:

  1. The first slab on the node partial list is designated as the new active slab
  2. Additional slabs are moved to the per-CPU partial list via put_cpu_partial(s, slab, 0) until the count exceeds cpu_partial_slabs / 2 (L2319) — so up to cpu_partial_slabs / 2 + 1 extra 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:

do_slab_free()

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:

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):

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:

put_cpu_partial()

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:

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:


References