Skip to main content
UltraInstinct
Back to latest articles
Cybersecurity13 min read

How SLUB Allocator Cross-Cache Attacks Work in Linux

Learn how SLUB allocator cross-cache attacks work in Linux by manipulating page frame recycling, CPU slab partials, and buddy allocator reclamation.

Featured visual representing How SLUB Allocator Cross-Cache Attacks Work in Linux
Advertisement
Google AdSense In-Article Contextual Slot

Introduction

Modern Linux kernel hardening has systematically eroded traditional heap exploitation primitives. Hardened freelists, randomized freelist pointers, dedicated caches for sensitive structures, and allocation order randomization have rendered intra-cache corruption increasingly unreliable. When an attacker identifies a Use-After-Free (UAF) or an out-of-bounds write within an isolated general-purpose slab—such as kmalloc-128 or a driver-specific cache—direct privilege escalation is frequently blocked by type isolation.

To bypass this isolation, advanced kernel exploitation relies on SLUB allocator cross-cache attacks. By exploiting the fundamental relationship between the SLUB layer and the underlying page-level Buddy Allocator, an attacker can manipulate page frame recycling. This enables the redirection of a dangling pointer in a low-value source cache to overlap directly with a high-value struct in a completely different target cache.

Understanding the mechanics of cross-cache attacks requires inspecting how the Linux memory subsystem manages page reclamation, CPU slab partials, and multi-order buddy page allocations. When software-level memory safety boundaries fail, enterprise workloads frequently rely on virtualization-level defenses or enclave architectures—as detailed in Hardware-Enforced Confidential Computing: Deep Microarchitectural Attestation, Memory Encryption Engines, and Enclave Security Pipelines—to constrain arbitrary kernel execution. However, within monolithic kernel space, mastering the cross-cache mechanism remains an essential discipline for both offensive researchers and kernel defense engineers.


Architecture of the Linux SLUB Allocator

The Linux kernel relies on the SLUB (Simple and Less Unruly Buster) allocator as its default implementation of the slab paradigm. The allocator is structured hierarchically across three distinct abstraction layers: the Buddy Allocator, the generic/dedicated kmem_cache descriptor, and per-CPU/per-node caches.

+-------------------------------------------------------------+
|                     Buddy Allocator                         |
|            (Order-N Physical Page Frame Blocks)             |
+-------------------------------------------------------------+
                               |
              alloc_pages()    |    free_pages()
                               v
+-------------------------------------------------------------+
|                  kmem_cache (SLUB Layer)                    |
|  +-------------------------------------------------------+  |
|  | kmem_cache_cpu: Active Slab & Local Free Lists        |  |
|  +-------------------------------------------------------+  |
|  | kmem_cache_node: Partial Lists (SLAB_PARTIAL)         |  |
|  +-------------------------------------------------------+  |
+-------------------------------------------------------------+
            |                                       |
            v                                       v
+-----------------------+               +-----------------------+
| kmalloc-128 (Source)  |               |  cred_jar (Target)    |
| [Obj 0][Obj 1][Obj 2] |               | [Cred][Cred][Cred]    |
+-----------------------+               +-----------------------+

The kmem_cache Structure

Every slab cache is governed by a struct kmem_cache. This structure stores metadata such as object size (size), allocation flags, alignment requirements, and offset values:

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    slab_flags_t flags;
    unsigned long min_partial;
    unsigned int size;             /* Object size including padding */
    unsigned int object_size;      /* Pure object size requested */
    struct kmem_cache_order_objects oo;
    struct kmem_cache_node *node[MAX_NUMNODES];
};

Physical pages are mapped into slabs using struct slab (historically tracked within struct page). Each slab tracks its freelist head, active allocation count (inuse), total object capacity (objects), and its parent cache:

struct slab {
    unsigned long flags;
    struct kmem_cache *slab_cache;
    void *freelist;                /* First free object pointer */
    unsigned counters;             /* inuse:16, objects:15, frozen:1 */
    struct list_head slab_list;
};

Freelist Navigation and Obfuscation

Within a single slab page, free objects are chained together via a singly linked list. Under CONFIG_SLAB_FREELIST_HARDENED, the freelist pointer is not stored in plaintext. Instead, the pointer is obfuscated using an XOR operation against a per-cache random cookie and the address of the pointer storage location itself:

$$\text{EncodedPtr} = \text{TargetPtr} \oplus \text{Cookie} \oplus \text{PointerAddress}$$

To bypass or avoid dealing with this cryptographic barrier, exploit primitives shift focus away from corrupting freelist pointers within the same slab. Instead, they corrupt raw functional structures by remapping the entire underlying page.


Anatomy of SLUB Allocator Cross-Cache Attacks

The core thesis of a cross-cache attack is transforming a page frame dedicated to a source cache (e.g., kmalloc-128, where an attacker has a memory corruption primitive) into a page frame serving a target cache (e.g., cred_jar, filp, or anon_vma).

[Phase 1: Source Allocation]
Slab Page Frame (Order-0) assigned to kmalloc-128
+-------------------+-------------------+-------------------+
|  Victim Object 0  | Vulnerable Obj 1  |  Victim Object 2  |
|  (In Use)         | (UAF Reference)   |  (In Use)         |
+-------------------+-------------------+-------------------+

                     |
                     | Attacker frees Objects 0, 1, and 2
                     | slab->counters.inuse reaches 0
                     v

[Phase 2: Slab Discard & Buddy Reclaim]
Slab is flushed from CPU partials to kmem_cache_node
discard_slab() -> free_pages() -> Buddy Allocator returns frame to Free List

                     |
                     | Target Cache runs out of partials
                     | alloc_pages() pulls recycled frame from Buddy
                     v

[Phase 3: Target Slab Instantiation]
Slab Page Frame re-assigned to cred_jar (or struct file)
+-------------------+-------------------+-------------------+
|    struct cred    |    struct cred    |    struct cred    |
|    (Attacker UID) |   (TARGET CREDS)  |   (Root UID: 0)   |
+-------------------+-------------------+-------------------+
        ^                     ^
        |                     |
   New Pointer          Stale Dangling Pointer from Phase 1
                        re-interprets cred memory!

The Slab Lifecycle and Buddy Deallocation

For a cross-cache attack to succeed, a slab page must be returned entirely to the Buddy Allocator. The SLUB allocator does not free an entire physical page back to the page allocator unless that slab's active reference count (slab->counters.inuse) drops to zero.

When an allocation request arrives:

  1. The kernel attempts to satisfy it from the local per-CPU cache (kmem_cache_cpu->freelist).
  2. If the active freelist is exhausted, it checks the per-CPU partial list (kmem_cache_cpu->partial).
  3. If the partial list is empty, it acquires locks on the node list (kmem_cache_node->partial).
  4. If no partially filled slabs exist on the node, it calls the Buddy Allocator:
    struct slab *new_slab = alloc_pages(gfp_mask, cache->oo.order);
    

Conversely, when an object is freed via kfree() or kmem_cache_free():

  1. inuse is decremented.
  2. If inuse == 0 and the slab is not the current active CPU slab, it is flagged for reclamation.
  3. If the slab resides on a partial list, and that partial list exceeds min_partial, the slab is detached from the cache's node list.
  4. The SLUB allocator invokes discard_slab(), which executes free_pages(), returning the physical memory block to the Buddy Allocator's free lists (zone->free_area[order]).

Step-by-Step Cross-Cache Exploitation Mechanics

Executing this primitive reliably requires deterministic heap grooming (spraying) to overcome non-deterministic kernel memory allocations.

Phase 1: Cache De-fragmentation and Mass Filling

Before triggering the vulnerability, the attacker must eliminate existing "holes" (partially populated slabs) across both the source and target caches. This prevents uncoordinated kernel background threads from reusing the freed memory locations.

/* Pseudocode: Spraying source objects to saturate existing partial slabs */
#define SPRAY_COUNT 1024

int fds[SPRAY_COUNT];
for (int i = 0; i < SPRAY_COUNT; i++) {
    // Allocate objects into source slab (e.g., via IPC message queues or socket buffers)
    fds[i] = allocate_source_slab_object();
}

The kernel's inter-process communication constructs, while optimized for low overhead similar to user-space designs explored in Designing Lock-Free Shared-Memory Ring Buffers: Cache-Coherence, Memory Barriers, and Kernel-Bypass IPC, introduce critical vulnerability surfaces when their backing pages are aliased across different caches.

Phase 2: Vulnerable Object Isolation

The attacker allocates an anchor slab containing the vulnerable object, surrounded by carefully tracked adjacent allocations:

int anchor_lead = allocate_source_slab_object();
int vulnerable_obj = allocate_vulnerable_object(); // Triggers UAF condition
int anchor_trail = allocate_source_slab_object();

At this stage, a dangling pointer references vulnerable_obj. However, simply releasing vulnerable_obj will not trigger page reclamation because anchor_lead and anchor_trail maintain slab->counters.inuse > 0.

Phase 3: Slab Draining

The attacker releases every object located within that specific physical slab page frame:

free_source_slab_object(anchor_lead);
free_vulnerable_object(vulnerable_obj); // Dangling pointer retained in user-space harness
free_source_slab_object(anchor_trail);

When the final object in the slab is released:

  • slab->counters.inuse transitions to 0.
  • The SLUB allocator unfreezes the slab.
  • The slab is removed from kmem_cache_node->partial and dispatched to discard_slab().
  • The Buddy Allocator places the physical frame into its CPU-local freelist (per_cpu_pages) or into the general order-0 free_area.
Buddy Allocator Free Lists:
order-0: [Page Frame X (Recycled)] -> [Page Frame Y] -> [Page Frame Z]

Phase 4: Target Cache Spraying and Page Recycling

Immediately following slab discarding, the exploit initiates a burst allocation targeting the desired structure. The target cache must be configured to demand new pages from the Buddy Allocator.

If the target cache matches the page order of the source cache (typically order-0, meaning single $4\text{ KB}$ pages), the Buddy Allocator operates in a Last-In, First-Out (LIFO) manner for its per-CPU page set. The recycled page frame is immediately handed over to construct the new slab for the target cache:

/* Spraying high-value structures (e.g., creds via fork/clone) */
for (int i = 0; i < TARGET_SPRAY_COUNT; i++) {
    pid_t pid = fork();
    if (pid == 0) {
        // Child process sleeps, holding its struct cred in cred_jar
        pause();
        exit(0);
    }
}

Because Page Frame X now belongs to the target cache, the dangling pointer from Phase 2 directly points to the memory offsets of the newly allocated target struct.


High-Value Target Structures: Pipe Buffers and Credential Objects

Depending on the primitive (UAF read, UAF write, double-free), attackers pivot the recycled page into specific kernel targets.

1. struct cred

The kernel credential structure is traditionally allocated from a dedicated slab cache (cred_jar). A cross-cache attack targeting cred_jar aims to overwrite fields directly:

struct cred {
    atomic_t usage;
    kuid_t uid;            /* Real UID */
    kgid_t gid;            /* Real GID */
    kuid_t suid;           /* Saved UID */
    kgid_t sgid;           /* Saved GID */
    kuid_t euid;           /* Effective UID */
    kgid_t egid;           /* Effective GID */
    kernel_cap_t cap_inheritable;
    kernel_cap_t cap_permitted;
    kernel_cap_t cap_effective;
    kernel_cap_t cap_bset;
    kernel_cap_t cap_ambient;
    ...
};

Using the dangling reference, writing zeros over the offset of uid, gid, euid, and egid, while saturating the cap_ fields with 0xFF, yields instant, stable root privileges for the process associated with that cred structure.

2. struct pipe_buffer

A second prominent target is struct pipe_buffer, allocated dynamically within kmalloc-1024 (or similar depending on ring size). It holds references to physical pages mapped into an IPC pipe:

struct pipe_buffer {
    struct page *page;
    unsigned int offset;
    unsigned int len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long private;
};

By targeting struct pipe_buffer:

  1. The attacker sprays pipe allocations using pipe().
  2. The recycled page lands inside kmalloc-1024.
  3. Through the dangling pointer, the attacker modifies pipe_buffer->ops to point to a crafted function pointer table, hijacking control flow.
  4. Alternatively, setting the PIPE_BUF_FLAG_CAN_MERGE flag transforms the primitive into an arbitrary read/write mechanism across read-only files (the classic "Dirty Pipe" design pattern).
/* Corrupting pipe_buffer via overlapping source write */
struct fake_pipe_buffer {
    void *page;
    unsigned int offset;
    unsigned int len;
    void *ops;             /* Direct kernel code execution pivot */
    unsigned int flags;
};

Cross-Cache Page Layout Simulation

The following low-level C implementation demonstrates how memory structures alias during a cross-cache exploit. This program models the exact memory offsets and physical page reclamation mechanics of a cross-cache primitive within user space:

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 

#define PAGE_SIZE 4096
#define SLUB_SOURCE_OBJ_SIZE 128
#define TARGET_OBJ_SIZE 128

typedef struct {
    char data[SLUB_SOURCE_OBJ_SIZE];
} source_obj_t;

typedef struct {
    uint32_t usage;
    uint32_t uid;
    uint32_t gid;
    uint32_t euid;
    uint32_t egid;
    uint64_t cap_effective;
    char padding[96];
} target_cred_t;

int main(void) {
    printf("[*] Simulating SLUB Allocator Cross-Cache Aliasing\n");

    // Allocate a page frame representing a fresh Buddy Allocator order-0 block
    void *buddy_page_frame = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (buddy_page_frame == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    printf("[+] Buddy Page Frame mapped at: %p\n", buddy_page_frame);

    // Step 1: Initialize the page as a kmalloc-128 source slab
    source_obj_t *source_slab = (source_obj_t *)buddy_page_frame;
    source_obj_t *vulnerable_dangling_ptr = &source_slab[2]; // Object at index 2

    strcpy(vulnerable_dangling_ptr->data, "MALICIOUS_UAF_PAYLOAD");
    printf("[+] Vulnerable object initialized in source cache. Stale ptr: %p\n", 
           (void *)vulnerable_dangling_ptr);

    // Step 2: Simulate slab draining and discard
    // All objects freed -> Buddy Allocator reclaims the frame
    memset(buddy_page_frame, 0x00, PAGE_SIZE);
    printf("[*] Slab drained. inuse dropped to 0. discard_slab() called.\n");

    // Step 3: Buddy Allocator reallocates the same frame to target cache (cred_jar)
    target_cred_t *target_slab = (target_cred_t *)buddy_page_frame;
    
    // Simulate initializing target objects (e.g., child process credentials)
    for (int i = 0; i < (PAGE_SIZE / TARGET_OBJ_SIZE); i++) {
        target_slab[i].usage = 1;
        target_slab[i].uid = 1001;          // Non-root user
        target_slab[i].gid = 1001;
        target_slab[i].euid = 1001;
        target_slab[i].egid = 1001;
        target_slab[i].cap_effective = 0x0; // Unprivileged
    }

    printf("[+] Recycled frame assigned to target cache (cred_jar).\n");
    printf("[+] Reading target cred state via target pointer:\n");
    printf("    Target Object [2] UID: %d, EUID: %d, Caps: 0x%lx\n",
           target_slab[2].uid, target_slab[2].euid, target_slab[2].cap_effective);

    // Step 4: Leverage the dangling pointer to overwrite the aliased target object
    printf("[!] Executing write via dangling source pointer...\n");
    
    // Attacker crafts overwrite payload matching offsets of target_cred_t
    target_cred_t *payload = (target_cred_t *)vulnerable_dangling_ptr;
    payload->uid = 0;                      // Overwrite UID to root (0)
    payload->gid = 0;                      // Overwrite GID to root (0)
    payload->euid = 0;                     // Overwrite EUID to root (0)
    payload->egid = 0;                     // Overwrite EGID to root (0)
    payload->cap_effective = 0xFFFFFFFFFFFFFFFF; // Elevate all capabilities

    // Step 5: Verify privilege escalation on the target structure
    printf("[+] Target credentials after corruption:\n");
    printf("    Target Object [2] UID: %d, EUID: %d, Caps: 0x%lx\n",
           target_slab[2].uid, target_slab[2].euid, target_slab[2].cap_effective);

    if (target_slab[2].uid == 0 && target_slab[2].cap_effective == 0xFFFFFFFFFFFFFFFF) {
        printf("[SUCCESS] Cross-cache corruption achieved privilege escalation!\n");
    } else {
        printf("[FAILURE] Memory layout mismatch.\n");
    }

    munmap(buddy_page_frame, PAGE_SIZE);
    return 0;
}

Modern Defenses: Random Slabs, SLAB_VIRTUAL, and Memory Cgroups

The kernel development community has introduced multiple deterministic defenses designed to thwart cross-cache exploitation.

+-------------------------------------------------------------+
|               Modern Linux Hardening Barriers               |
+-------------------------------------------------------------+
| 1. CONFIG_RANDOM_KMALLOC_CACHES: Multiple kmalloc paths     |
|    breaks deterministic spraying of source objects.         |
+-------------------------------------------------------------+
| 2. Memory Cgroups (memcg) Accounting:                       |
|    Isolates user-controlled allocations from root tasks.   |
+-------------------------------------------------------------+
| 3. SLAB_VIRTUAL / Page-to-Slab Type Isolation:              |
|    Dedicated virtual memory ranges per kmem_cache type      |
|    prevent physical page reuse across distinct caches.      |
+-------------------------------------------------------------+

1. CONFIG_RANDOM_KMALLOC_CACHES

Merged to mitigate predictable slab placement, CONFIG_RANDOM_KMALLOC_CACHES instantiates multiple distinct copies of general-purpose kmalloc slabs (e.g., 16 separate kmalloc-128 caches). When an object is allocated via kmalloc(), the kernel selects a cache based on a hash of the calling code address:

$$\text{Index} = \text{Hash}(\text{ReturnAddress}) \pmod{\text{NUM_CACHES}}$$

This deterministic hashing ensures that spray objects allocated via IPC calls or network sockets reside in a different kmalloc-X instance than the vulnerable object allocated by a buggy driver. As a result, an attacker cannot easily control the neighboring objects within the slab to force its active count to zero.

2. Memory Cgroup (MemCG) Slab Isolation

Modern kernels enforce strict memory cgroup boundaries. Allocations made within an unprivileged container or user cgroup are tagged with the allocating cgroup's charging structures. If a slab page is allocated by a user thread, it cannot be reclaimed and handed off to satisfy root-level system calls in the host root cgroup without triggering explicit charging faults and slab separation:

/* Slab allocation charging */
static inline struct slab *alloc_slab_page(gfp_t flags, int order, struct kmem_cache *s) {
    struct slab *slab = (struct slab *)alloc_pages(flags, order);
    if (slab && memcg_kmem_enabled())
        memcg_alloc_slab_cgroups(slab, s, flags);
    return slab;
}

3. Dedicated Slab Virtual Addressing (SLAB_VIRTUAL)

The most comprehensive defense against cross-cache attacks is architectural address space separation. Under proposals like SLAB_VIRTUAL and AUTOSLAB, the kernel reserves separate virtual memory zones for distinct slab caches.

Even if a physical page frame is returned to the Buddy Allocator and reassigned to a different slab, its virtual address translation mapping changes. Any existing dangling virtual address pointers become invalid or trigger page faults (SIGSEGV in user-space or PAGE_FAULT panic in the kernel) because the old virtual address is permanently unmapped from the underlying physical page frame.

For mission-critical deployments where kernel integrity must be verifiably asserted against unauthorized runtime modifications, platforms integrate Hardware-Rooted Remote Attestation in Confidential AI Pipelines: Cryptographic Verification and Microarchitectural Isolation alongside memory isolation, providing verifiable measurements of the executing kernel images and page structures.


Conclusion

SLUB allocator cross-cache attacks represent an evolution in Linux kernel exploitation. Rather than attempting to corrupt obfuscated freelist metadata within an isolated, low-value cache, cross-cache attacks target the interface between the SLUB allocator and the Buddy Allocator. By draining a slab of all active allocations, an attacker forces the deallocation of the backing physical page frame, allowing it to be recycled by a target cache containing security-critical structures like struct cred or struct pipe_buffer.

Mitigating these attacks has required foundational changes to the Linux memory subsystem. Techniques such as CONFIG_RANDOM_KMALLOC_CACHES, strict cgroup accounting, and page-to-slab virtual address isolation demonstrate that securing the heap can no longer rely solely on intra-slab metadata protections. Modern kernel defense requires structural isolation at the page allocator layer to ensure that when a slab page is released, its security boundary remains intact.


References