algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

cross-cache attacks — reaching the objects in their own cache

2026-04-163 min readkernel pwn

Heap spray works beautifully — until the object you want to corrupt lives in its own kmem_cache. A UAF in kmalloc-256 can't reclaim a struct cred (own cache: cred_jar) or a struct file (own cache: filp) by spraying, because SLUB serves each cache from separate slabs. Cross-cache attacks demolish that isolation by operating one level down — at the page.

target
problem
the victim UAF is in cache A; the object you want is in cache B
isolation
SLUB never mixes two caches' objects on one slab
lever
but slabs are just pages, and the buddy allocator recycles pages across caches

SLUB sits on top of the page allocator#

A kmem_cache doesn't own memory forever. It requests slabs — runs of pages — from the buddy (page) allocator, carves them into objects, and when a slab becomes entirely free, SLUB can hand that page back to the buddy allocator. The buddy allocator has no notion of caches: the next code to ask for a page of that order gets it, whatever cache it's for. That return-and-reuse is the whole attack.

the cross-cache primitivetxt
1
2
3
4
5
1. groom cache A so the victim sits on a slab you can empty
2. free EVERY object on that slab  ->  the slab page goes back to the buddy allocator
3. immediately make cache B demand new slabs (allocate many B objects)
4. cache B is handed the SAME page  ->  B's objects now overlap A's freed slot
5. your dangling cache-A reference now points INTO a cache-B object  (e.g. a cred)

the choreography#

The hard part is step 2 — emptying the victim's slab so the whole page frees, not just one object. You groom: allocate enough victims to fill a fresh slab, arrange the dangling reference to one of them, then free the entire slab's worth so SLUB releases the page:

grooming an empty slab so the page is releasedtxt
1
2
3
4
5
spray A:   fill a brand-new slab with N victim objects (N = objects-per-slab)
dangle:    keep a UAF reference to one victim on that slab
free A:    release all N  ->  slab empty  ->  page returned to buddy allocator
claim B:   allocate cred/file/pipe_buffer objects until cache B grabs that page
overlap:   the dangling A pointer and a fresh B object share bytes

Now a UAF that could only ever corrupt a kmalloc-256 object reaches a struct cred — and overwriting the right bytes of a cred is root, without ever leaking an address or corrupting a pointer the mitigations watch.

a kmalloc UAF that ends up inside cred_jar
txt
1
2
3
4
5
[*] filled victim slab (kmalloc-256), 16 objs/slab
[*] freed all 16 -> order-1 page released to buddy
[*] sprayed 512 struct cred -> cred_jar reclaimed the page
[*] dangling ref now overlays a cred; uid patched -> uid=0
segfault{the_page_does_not_care_which_cache}

why it matters now, and what fights it#

  • It's the answer to dedicated caches. Modern hardening puts dangerous structs in isolated caches precisely to stop reclaim-by-spray; cross-cache steps under that isolation at the page layer, so it's the default technique in current kernel exploits.
  • It composes with DirtyCred. Cross-cache to get a cred/file page where you want it, then swap the privileged object in.
  • The defences are page-level too. CONFIG_SLAB_VIRTUAL, per-cache page pools, and not returning slabs to the buddy allocator promptly all raise the cost; the attack adapts by grooming harder and racing the page reuse.

The mental shift from userland: in glibc you reshape bins; in the kernel you reshape slabs and pages. SLUB's caches look like strong isolation until you remember they're all renting from the same page allocator — and a page you free in one cache is a page another cache will gladly reuse.