Linux Kernel Internals · Security Research

SLUB Allocator & Cross-Cache Attack

// Linux Kernel 6.x — Per-CPU Caches, Partial Slabs, Full Slabs, Type Confusion
kernel 6.x SLUB allocator heap exploitation cross-cache attack CVE research
Overview Arsitektur

SLUB Allocator — Tiga Tier

Linux kernel 6.x menggunakan SLUB sebagai default slab allocator. Setiap kmem_cache merepresentasikan pool objek untuk ukuran atau tipe tertentu. Alokasi mengalir melalui tiga tier untuk meminimalkan locking.

① Per-CPU Cache (fast path)

Lock-free. Setiap CPU punya kmem_cache_cpu dengan freelist lokal. Alokasi ~10ns. Tidak butuh lock sama sekali selama freelist tidak kosong.

② Partial Slab List (medium path)

Slab dengan campuran slot allocated + free. Ada versi per-CPU (no lock) dan per-node (butuh list_lock). Diisi ulang ke per-CPU freelist saat habis.

③ Buddy Allocator (slow path)

Jika semua partial habis, SLUB minta page baru dari buddy allocator via alloc_pages(). Page baru dijadikan slab baru untuk cache tersebut.

Visualisasi Tier
// kmem_cache: kmalloc-64 (contoh)
CPU 0 — struct kmem_cache_cpu
freelist [obj_A] → [obj_C] → [obj_F] → NULL
slab slab_1 (frozen=1, inuse=5/8)
partial slab_2 → NULL (cpu_partial, no lock)
slab_1 (frozen=1)
↕ drain / refill
Node 0 — kmem_cache_node->partial (list_lock)
slab_3 inuse=3/8
slab_4 inuse=1/8
→ NULL
↕ alloc_pages / free_pages
Buddy Allocator (struct page / order-based)
Full slabs tidak di-track di list manapun — SLUB optimization. Ketika inuse → 0, slab langsung discard_slab() → __free_pages()
Per-CPU Cache Detail

struct kmem_cache_cpu

Ini adalah hot path. Hampir semua alokasi kernel selesai di sini tanpa menyentuh lock apapun. Kunci implementasinya adalah TID (transaction ID) sebagai lockless synchronization.

Layout Struct
Offset
Field
Keterangan
+0x00
void **freelist
Ptr ke free obj pertama. Isi objek = ptr ke next free obj (encoded)
+0x08
ulong tid
Transaction ID — increment per operasi. Deteksi race tanpa lock
+0x10
struct slab *slab
Active slab. frozen=1 artinya dipegang CPU ini ← eksploitasi target
+0x18
struct slab *partial
Per-CPU partial list (CONFIG_SLUB_CPU_PARTIAL). Lock-free buffer
Fast Path — kmem_cache_alloc()
mm/slub.c — disederhanakan
// Setiap CPU baca freelist-nya sendiri (RCU-protected read) void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags) { // === FAST PATH === void *object = this_cpu_read(s->cpu_slab->freelist); ulong tid = this_cpu_read(s->cpu_slab->tid); if (likely(object)) { // Pop object dari freelist (freepointer ada di dalam object) void *next = get_freepointer_safe(s, object); // cmpxchg: atomic update freelist + tid sekaligus if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, object, tid, next, next_tid(tid)))) // gagal jika ada race goto redo; return object; // ≈10-15ns, NO LOCK } // === SLOW PATH: __slab_alloc() === // 1. Coba refill dari per-CPU partial (masih no lock) // 2. Coba ambil dari node->partial (butuh node->list_lock) // 3. Alokasi slab baru: new_slab() → alloc_pages(GFP_*) return __slab_alloc(s, gfpflags, addr); }
Relevansi exploit: Ketika freelist pointer di-corrupt via UAF, alokasi berikutnya mengembalikan arbitrary address sebagai "objek valid". TID mempersulit race-based attack tapi tidak mencegah freelist poisoning jika kamu sudah punya write primitive.
Partial & Full Slabs

struct slab — Kernel 6.x

Sejak kernel 5.17, struct page untuk slab sudah di-refactor menjadi struct slab tersendiri. Field frozen dan inuse sangat penting untuk memahami lifecycle slab.

Offset
Field
Keterangan eksploitasi
+0x00
ulong __page_flags
Page flags — termasuk PG_slab untuk identifikasi
+0x08
kmem_cache *slab_cache
← Pointer ke parent cache. Corruption di sini = type confusion cache-level
+0x10
void *freelist
← Freelist in-slab. Target utama untuk UAF info leak
+0x18
uint inuse:16
Jumlah objek allocated. inuse==0 → slab dibuang ke buddy
+0x18
uint objects:15
Total kapasitas slot dalam slab ini
+0x18
uint frozen:1
frozen=1 = dipegang per-CPU. frozen=0 = bisa di-reclaim → cross-cache!
Lifecycle Slab
① New slab — alloc_pages(order)
Buddy allocator berikan 2^order pages → dijadikan slab baru. frozen=1, inuse=0
② Active — dipasang ke cpu_slab
frozen=1, inuse bertambah setiap kmem_cache_alloc(). Full = inuse==objects
③ Partial — ada slot free dan allocated
frozen=0. Masuk cpu_partial (no lock) atau node->partial (list_lock). Bisa di-promote kembali ke active
④ Empty — inuse == 0 → discard_slab()
__free_pages(page, order). Page KEMBALI KE BUDDY ALLOCATOR. Cache lain bisa claim page ini ← cross-cache!
Tunable Penting
Baca via /sys/kernel/slab/kmalloc-64/
# Seberapa banyak objek di cpu_partial sebelum drain ke node cat /sys/kernel/slab/kmalloc-64/cpu_partial # default: 13 # Minimum slab di node partial sebelum free ke buddy cat /sys/kernel/slab/kmalloc-64/min_partial # default: 5 # Jumlah objek per slab cat /sys/kernel/slab/kmalloc-64/objs_per_slab # Lihat semua cache aktif: cat /proc/slabinfo | grep "^kmalloc"
Freelist Internals

Freepointer & FREELIST_HARDENED

Freepointer disimpan di dalam objek yang sedang free — di offset s->offset. Di kernel 6.x dengan CONFIG_SLAB_FREELIST_HARDENED, pointer di-encode dengan XOR untuk mempersulit poisoning.

Tanpa Hardening (old / CONFIG=n)
obj_A
*next=obj_C
obj_C
*next=obj_F
obj_F
*next=NULL
NULL
Freelist poisoning — tanpa hardening
// Objek sudah di-free, pointer kita masih valid (UAF) struct vuln_obj *uaf = get_stale_ptr(); // Offset 0 dari objek = freepointer (jika s->offset == 0) *(ulong *)uaf = target_addr; // poison freelist! // Alokasi 1: mengembalikan uaf (normal) void *a = kmem_cache_alloc(cache, GFP_KERNEL); // Alokasi 2: mengembalikan target_addr ← ARBITRARY ALLOC! void *b = kmem_cache_alloc(cache, GFP_KERNEL); // = target_addr
Dengan Hardening (kernel 6.x default)
CONFIG_SLAB_FREELIST_HARDENED — encoding freepointer
// include/linux/slub_def.h static inline void *get_freepointer(struct kmem_cache *s, void *object) { return freelist_ptr_decode(s, *(void **)(object + s->offset), (ulong)object + s->offset); } static inline void *freelist_ptr_decode(struct kmem_cache *s, void *ptr, ulong ptr_addr) { // Encoded = ptr XOR s->random XOR ptr_addr return (void *)((ulong)ptr ^ s->random ^ ptr_addr); } // Untuk poison freelist dengan hardening — harus encode dulu: ulong encoded = target_addr ^ s->random ^ ((ulong)uaf + s->offset); *(ulong *)((char*)uaf + s->offset) = encoded;
Bypass: Butuh leak s->random terlebih dahulu. Bisa didapat dari: (1) arbitrary read yang sudah ada sebelumnya, (2) heap spray yang expose partial freelist dari slab lain, (3) kernel info leak dari /proc/kallsyms + pointer arithmetic jika KASLR bypass sudah ada.
FREELIST_RANDOM

CONFIG_SLAB_FREELIST_RANDOM — urutan objek dalam slab baru di-randomize. Ini mempersulit prediksi slot mana yang akan di-alokasikan berikutnya, tapi spray massal (ratusan alokasi) masih bisa overcome ini secara statistik.

Cross-Cache Attack

Type Confusion via Page Reclaim

Cross-cache memanfaatkan satu fakta fundamental: ketika slab kosong, page fisiknya dikembalikan ke buddy allocator dan bisa di-claim oleh cache yang sama sekali berbeda. Stale pointer ke objek lama akan menunjuk ke objek tipe berbeda.

Step-by-Step Interaktif
1

Spray Cache A (kmalloc-64)

Alokasikan banyak objek di cache target (contoh: via sendmsg, setsockopt, atau syscall lain). Tujuan: isi beberapa slab agar kita punya kontrol atas layout memori.

Slab state — kmalloc-64:
slab_Ainuse=8/8
slab_Binuse=6/8
Semua 8 slot di slab_A terisi. Slot terakhir adalah objek yang akan kita UAF.
2

Trigger UAF — Stale Pointer

Free sebagian besar objek. Pertahankan pointer ke objek target (slot 7). Objek sudah di-free secara internal (masuk freelist), tapi kita masih bisa baca/tulis via stale pointer.

Slab state setelah free bulk:
slab_Ainuse=1/8
stale_ptr = 0xffff888012340700 ← masih pointing ke slot 7
*stale_ptr = [objek sudah di-free, tapi readable/writable!]
UAF: kernel sudah anggap objek ini free, tapi pointer userspace/kernel kita masih valid. Read = info leak. Write = freelist poisoning.
3

Drain Slab → Page ke Buddy

Free objek UAF terakhir (slot 7). Sekarang inuse=0. SLUB memanggil discard_slab()__free_pages(). Page fisik dikembalikan ke buddy allocator.

slab_AEMPTY → discard!
Buddy Allocator
Page 0xffff888012340000
order=0, siap di-claim
// mm/slub.c static void discard_slab(struct kmem_cache *s, struct slab *slab) { dec_slabs_node(s, slab_nid(slab), slab->objects); free_slab(s, slab); // → __free_pages() }
4

Spray Cache B — msg_msg / pipe_buffer

Alokasikan banyak objek dari cache B (ukuran kompatibel). Buddy allocator sangat mungkin memberikan page yang sama ke slab baru cache B. Objek cache B menempati slot yang persis sama dengan objek cache A sebelumnya.

slab_A*SAME PAGE — cache B!
stale_ptr = 0xffff888012340700 ← masih ada!
*stale_ptr = msg_msg[7] ← sekarang pointing ke struct msg_msg!
Page yang sama sekarang milik cache B. Stale pointer kita menunjuk ke struct msg_msg — type confusion berhasil!
5

Type Confusion → Arbitrary Read/Write

Dengan menulis ke slot via UAF stale pointer, kita corrupt field msg_msg.next atau msg_msg.m_ts. Lalu msgrcv() akan baca memori dari address yang kita tentukan.

Layout msg_msg (64 bytes pertama)
Offset
Field
Exploit role
+0x00
m_list.next
list_head — overwrite untuk list manipulation
+0x08
m_list.prev
list_head
+0x10
m_type
long — bebas diset
+0x18
m_ts
size_t — set besar untuk baca lebih banyak dari next ptr!
+0x20
*next
msg_msgseg ptr ← ARAHKAN KE TARGET ADDRESS → arbitrary read!
+0x28
*security
LSM blob — info leak jika non-null
Arbitrary read primitive via msgrcv
// Tulis ke slot via UAF stale pointer struct fake_msg { char pad[0x18]; // skip m_list + m_type size_t m_ts = 0x1000; // baca 4KB dari next void *next = target_kaddr; // ← kernel address target }; memcpy(stale_ptr, &fake_msg, sizeof(fake_msg)); // UAF write // Read: kernel akan copy dari target_kaddr ke userspace! char leak[0x1000]; msgrcv(msqid, leak, 0x1000, 0, IPC_NOWAIT|MSG_NOERROR|MSG_COPY); // leak[] sekarang berisi konten dari target_kaddr → KASLR bypass!
Spray Primitives

Objek Kernel — Target Cross-Cache

Pilihan objek spray yang tepat menentukan primitif apa yang bisa dicapai. Tabel di bawah adalah referensi untuk kernel 6.x berdasarkan cache yang digunakan dan primitif yang bisa dieksploitasi.

Struct Cache / Ukuran Syscall Primitif Relevansi
msg_msg kmalloc-{64..4k} msgsnd / msgrcv Arb read (next ptr), arb write CVE-2021-22555, ksmbd
pipe_buffer kmalloc-64 pipe() RIP control via ops ptr Dirty Pipe family
sk_buff kmalloc-{256,512} setsockopt, sendmsg Arb write via skb data Ukuran adjustable
user_key_payload kmalloc-{32..16k} add_key() Heap shaping, hole punching Size sangat fleksibel
setxattr buf kmalloc-* (1-65535) setxattr() Spray / defragment Ukuran bebas
timerfd_ctx kmalloc-256 timerfd_create() Func ptr (wqh.func) RIP via callback
seq_operations kmalloc-32 open(/proc/...) RIP control (start ptr) CVE-2021-41073
file (struct) filp cache open() f_op func ptr Dedicated cache
Contoh Spray Pattern — msg_msg
heap spray + hole punching untuk cross-cache setup
// 1. Buat banyak message queue int msqids[NUM_QUEUES]; for (int i = 0; i < NUM_QUEUES; i++) msqids[i] = msgget(IPC_PRIVATE, 0644 | IPC_CREAT); // 2. Spray msg_msg ke kmalloc-64 (size = 64 - sizeof(msg_msg_hdr)) char payload[16] = {0}; // 16 bytes data → total msg = 48+16 = 64 for (int i = 0; i < NUM_QUEUES; i++) msgsnd(msqids[i], &payload, sizeof(payload), 0); // 3. Hole punching — free setiap-dua untuk buat fragment for (int i = 0; i < NUM_QUEUES; i += 2) msgctl(msqids[i], IPC_RMID, NULL); // buat hole // 4. Trigger alokasi cache A di hole yang terbentuk // → page yang sama sekarang shared antara msg_msg dan vuln_obj
Relevansi untuk ksmbd & FUSE

ksmbd targets

ksmbd_work, response buffer (kmalloc-512/kmalloc-1024), session struct. UAF di SMB2 path → cross-cache ke msg_msg. Tidak butuh privilege — bisa trigger dari network.

FUSE targets

fuse_req dari kmalloc-256. Accessible tanpa CAP_SYS_ADMIN via user namespace. UAF di passthrough path bisa cross-cache ke timerfd_ctx atau sk_buff.

Mitigasi di Kernel 6.x

Defense vs Bypass

CAN BYPASS
CONFIG_SLAB_FREELIST_HARDENED
XOR-encode freepointer dengan s->random dan address objek. Mencegah freelist poisoning tanpa mengetahui secret.
Bypass: Leak s->random via arbitrary read primitif sebelumnya. Chain: infoleak → leak random → poisoning.
CAN BYPASS
CONFIG_SLAB_FREELIST_RANDOM
Urutan objek dalam slab baru di-randomize saat alloc. Mempersulit prediksi slot mana yang akan dapat objek berikutnya.
Bypass: Spray massal (100+ alokasi) secara statistik overcome randomisasi ini.
PARTIAL
CONFIG_RANDOM_KMALLOC_CACHES (6.6+)
Beberapa varian cache untuk kmalloc-N dipilih random per-alokasi. Cross-cache jadi lebih sulit karena spray bisa landing di varian berbeda.
Bypass: Gunakan dedicated cache (bukan kmalloc generic), atau exploit via order-based allocator yang tidak terpengaruh.
PARTIAL
INIT_ON_ALLOC / INIT_ON_FREE
Zero-fill objek saat alloc atau free. Menghilangkan stale data info leak dari objek yang baru di-alloc.
Bypass: Tidak mencegah type confusion itu sendiri — hanya hilangkan stale data. Primitif tulis tetap bisa corrupt struct baru.
PARTIAL
CONFIG_MEMCG Isolation
Per-cgroup memory accounting dapat isolasikan cache antar cgroup berbeda, mencegah cross-cache lintas cgroup.
Bypass: Dalam satu cgroup (umum di container), tidak ada isolasi. Cross-cache tetap bisa dalam scope yang sama.
EFFECTIVE
KASLR + CFI (kCFI)
KASLR randomize base kernel. kCFI (kernel Control Flow Integrity) validasi target indirect call. Keduanya mempersulit RIP control.
Masih bisa bypass dengan infoleak KASLR dulu, lalu gunakan ROP chain yang valid secara CFI. Kompleksitas tinggi.
Exploitation Chain Lengkap (kernel 6.x)
① UAF / OOB di cache target
Dari syzkaller crash, manual fuzzing, atau known CVE path
② Heap spray & massage
Defragment heap, kontrol layout, pastikan slab berisi objek yang kita kendalikan
③ Cross-cache → msg_msg
Drain slab, spray msg_msg, achieve type confusion via stale pointer
④ Arbitrary read → KASLR bypass
Corrupt msg_msg.next, msgrcv dari kernel text/data address, dapatkan kernel base
⑤ Leak s->random (jika hardened)
Baca kmem_cache struct dari kernel address yang sudah diketahui
⑥ Freelist poisoning → arbitrary alloc → overwrite cred/modprobe_path
kmem_cache_alloc mengembalikan arbitrary address → tulis 0 ke uid/gid atau overwrite modprobe_path
⑦ Root / LPE achieved
commit_creds(prepare_kernel_cred(0)) atau eksekusi via modprobe_path trigger
Interactive Simulator

SLUB Allocator Simulator

Simulasi interaktif alokasi / free / spray pada slab. Klik tombol untuk melihat bagaimana state slab berubah dan bagaimana cross-cache terjadi.

CPU Freelist State
Log
Tip: Coba urutan ini untuk simulasikan cross-cache: Alloc A 8x → Mark UAF (slot terakhir) → Free A 8x → Alloc cache B → perhatikan log "CROSS-CACHE: page reuse!"