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

kernel heap spray — the elastic objects you allocate on demand

2026-04-144 min readkernel pwn

The slab UAF and every kernel use-after-free that follows share one requirement: after you free the victim, you need to reclaim its slot with bytes you control. That's a spray, and the kernel hands you several syscalls that allocate objects of an attacker-chosen size and content. Knowing this small zoo — which object, what size, how long it lives — is most of practical kernel heap exploitation.

target
allocator
SLUB (kmalloc-8 .. kmalloc-8k, plus dedicated caches)
need
allocate an object of a chosen size, filled with chosen bytes, on command
the catch
lifetime — some sprays are persistent, some vanish unless you pin them

what makes an object useful#

You want elastic objects: the size is controllable (so you can hit any kmalloc-N cache the victim lives in) and the content is controllable (so you can forge whatever the victim's bytes mean — a function pointer, a length, a reference count, a type tag). Four cover almost everything:

the spray toolkittxt
1
2
3
4
5
6
object             syscall            size          content     lifetime
-----------------  -----------------  ------------  ----------  --------------------
msg_msg            msgsnd/msgrcv      ~48 .. 4096   full*       persistent (in queue)
user_key_payload   add_key/keyctl     controlled    full        persistent (keyring)
xattr buffer       setxattr           controlled    full        transient (freed fast)
sk_buff data       send/recv socket   controlled    full**      persistent (in queue)

msg_msg — the workhorse#

System V message queues allocate a struct msg_msg header plus your data in one kmalloc of a size you pick by message length. The data is fully attacker- controlled, it stays allocated until you receive it, and — because msg_msg carries a m_ts length and a next pointer for segmented messages — a partial overwrite of those fields turns into an OOB read or a second UAF:

spray controlled bytes into kmalloc-Nc
1
2
3
4
5
struct { long mtype; char data[N - 0x30]; } m;   // -0x30: the msg_msg header
m.mtype = 1; memset(m.data, 0x41, sizeof m.data);
for (int i = 0; i < SPRAY; i++)
    msgsnd(qid[i], &m, sizeof m.data, 0);         // each lands in kmalloc-N
// msgrcv(qid, ..., MSG_COPY|IPC_NOWAIT) peeks WITHOUT freeing -> read back/leak

add_key — persistent, simple, reliable#

add_key("user", ...) allocates a user_key_payload with your size and bytes and keeps it alive in the keyring. No header math, no queue bookkeeping — the go-to when you just need "N bytes of my data, parked, until I revoke it."

setxattr — total control, but it evaporates#

setxattr kmallocs a buffer of your exact size, copies your exact bytes in, does its work, and frees it immediately. Perfect content control, useless lifetime — unless you stop time. Register the source buffer with userfaultfd or FUSE so the copy_from_user blocks mid-copy: the allocation now sits there, filled with your data, frozen for as long as you hold the fault. That pairing — setxattr for content, userfaultfd for lifetime — is a staple.

the spray pattern#

The choreography around a UAF is always the same shape:

free -> reclaim -> controltxt
1
2
3
4
5
1. allocate the victim (a struct with a function ptr / refcount / type field)
2. trigger the bug to free it (UAF / double-free), keep a dangling reference
3. SPRAY: msgsnd / add_key x200  of size == the victim's kmalloc cache
4. one spray object lands in the freed slot -> its bytes ARE the victim now
5. use the dangling reference: the kernel reads YOUR forged function ptr / length
the freed slot, now full of my bytes
txt
1
2
3
[*] victim (kmalloc-256) freed; 200x msg_msg sprayed
[*] dangling ->ops->ioctl now reads 0x4141414141414141  (our spray)
[*] reclaimed: control of the victim object achieved

what stands in the way#

  • Dedicated caches. Many juicy structs (cred, file, task_struct) live in their own kmem_cache, so a kmalloc-N spray can't reach them. That's what cross-cache attacks are for.
  • Cache randomization & hardening. CONFIG_SLAB_FREELIST_RANDOM and RANDOM_KMALLOC_CACHES perturb which slot you get; you spray more and rely on statistics, or groom deterministically.
  • CONFIG_SLAB_VIRTUAL / autoslab and freelist hardening raise the bar on what a freed pointer can become.

These syscalls are the kernel-pwn equivalent of malloc(size) with attacker bytes. Once you can put your data, at your size, into the freed slot of a victim object, every UAF becomes a question of what the victim's bytes mean — and the next two parts are about reaching the victims that don't live in a kmalloc-N cache at all.