Skip to content

io_uring UAF (CVE-2021-41073)

loop_rw_iter()'da eksik bir buffer-select kontrolü, bir kernel io_buffer pointer'ını sanki userspace adresiymiş gibi ilerletir; böylece io_put_kbuf() sonradan kmalloc-32'de attacker'ın seçtiği bir offset'i free eder — use-after-free LPE'ye dönüşen kontrol edilebilir bir free.

Mechanism

Note

io_uring, bir request'in data buffer'ını IOSQE_BUFFER_SELECT SQE flag'i üzerinden önceden register edilmiş bir havuzdan seçmesine izin verir. Seçildiğinde kernel, req->rw.addr'da bir struct io_buffer'a (kmalloc-32'de 32-byte'lık bir object) bir pointer saklar ve REQ_F_BUFFER_SELECT'i set eder. Kodun korumakta olması gereken invariant: BUFFER_SELECT ile işaretlenmiş bir req->rw.addr bir kernel pointer'ıdır ve asla bir userspace iov adresiymiş gibi ilerletilmemeli. Bug şu: loop_rw_iter()read_iter/write_iter olmayan dosyalar için kullanılan fallback read/write döngüsü (örneğin birçok /proc dosyası) — her transfer'den sonra flag'i kontrol etmeden req->rw.addr += nr; yapar ve kernel io_buffer pointer'ını taşınan byte sayısı kadar ilerletir.

Döngüden sonra completion, io_put_rw_kbuf()io_put_kbuf() çalıştırır; bu da artık ilerletilmiş req->rw.addr'dan türetilen kbuf'u kfree() eder. Attacker nr'yi (transfer boyutu) kontrol ettiğinden eklenen offset'i kontrol eder, yani orijinal io_buffer'dan seçilmiş bir kayma mesafesindeki bir pointer'ı free eder:

"etkin bir şekilde kmalloc-32 cache'inde, başlangıçta allocate ettiğimiz buffer'dan kontrol edilebilir bir offset'te buffer'ları free etme yeteneği kazanırız."

Bu, canlı kernel belleğinin kontrollü bir free'sidir. Free edilmiş slot'un hâlâ başka bir yerde canlı bir reference'ı varsa, kmalloc-32'de bir use-after-free / type confusion'a dönüşür. Zafiyetli kod fs/io_uring.c'de, Linux 5.10'dan 5.14.6'ya kadar etkiliyor.

İlgili object:

struct io_buffer {
    struct list_head list;   /* offset 0 — first member; leaks heap pointers */
    __u64 addr;
    __u32 len;
    __u16 bid;
};                           /* exactly 32 bytes -> kmalloc-32 */

io_buffer'lar IORING_OP_PROVIDE_BUFFERS ile oluşturulur; bu aynı zamanda heap-grooming primitive'i olarak da iş görür.

Walkthrough

Uçtan uca, chompie exploit'ini izleyerek:

  1. Groom. io_uring_prep_provide_buffers() üzerinden ~1000 io_buffer spray'le; kmalloc-32'yi tüket ve öngörülebilir bitişiklik / taze slab page'leri elde et.
  2. Yan yana getir. Free ve reclaim aynı SLUB per-CPU slab'ine vurmalı, bu yüzden ring'i IORING_SETUP_SQPOLL ile kur, worker'ları IORING_REGISTER_IOWQ_AFF ile pinle ve exploit thread'lerini sched_setaffinity() ile pinle (bkz. ./io-uring-sqpoll-technique.md).
  3. Kontrollü free'yi tetikle. Bir loop_rw_iter dosyası (örneğin bir /proc dosyası) üzerinde IOSQE_BUFFER_SELECT ile bir read submit et; döngü sonrası req->rw.addr += nr pointer'ı kaydırır ve completion seçilmiş bir kmalloc-32 slot'unu kfree() eder.
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, proc_fd, NULL, len, 0);
sqe->flags |= IOSQE_BUFFER_SELECT;   /* req->rw.addr becomes a kernel io_buffer ptr */
sqe->buf_group = GID;
io_uring_submit(&ring);              /* loop_rw_iter advances the kernel ptr, then kfrees it */
  1. Reclaim (universal heap spray). Free edilmiş 32-byte'lık slot'u, copy_from_user'ı bir FUSE read ile durdurulan setxattr() kullanarak attacker-kontrollü içerikle yeniden allocate et (vm.unprivileged_userfaultfd=0 olduğunda userfaultfd'nin yerine geçer). Bloklanan setxattr, free edilmiş slot üzerinde kontrollü bir 32-byte object'i süresiz canlı tutar.

  2. Leak (universal heap leak). setxattr'ı blokla, UAF free'sini tetikle, slot'a bir target object allocate et, bloku kaldır ve getxattr() üzerinden geri oku. Kullanışlı overlay hedefleri:

  3. io_tctx_nodetask alanı process task_struct'ına işaret eden per-thread io_uring node'u.
  4. io_buffer.list (offset 0) — sprayed-object adreslerini hesaplamak için göreli heap konumunu sızdırır.
  5. seq_operations (/proc/cmdline'ı aç) — single_next() function pointer'ını sızdırmak KASLR'ı yener.

  6. Code execution. prog'u sahte bir bpf_prog'a işaret eden bir sk_filter overlay'le/forge et; sahte bpf_func / insns bir interpreter veya ROP payload çalıştırarak cred->uid/cred->euid'i overwrite eder → root.

Spray'i boyutlandırmak için slab geometrisini okuma
$ cat /sys/kernel/slab/kmalloc-32/objs_per_slab
128
$ cat /sys/kernel/slab/kmalloc-32/cpu_partial
30
# allocate objs_per_slab * (cpu_partial + 1) io_buffers to fully groom

Warning

Free, worker context'inde olur, bu yüzden aynı-CPU pinning olmadan reclaim spray'i (senin syscall thread'inde çalışan) farklı bir slab'e düşer ve UAF sessizce başarısız olur. SQPOLL + IORING_REGISTER_IOWQ_AFF + sched_setaffinity üçlüsü opsiyonel değil, zorunludur. Ayrıca: nr (ve dolayısıyla free edilen offset) öyle seçilmeli ki ilerletilen pointer map'li slab belleğinin içinde kalsın, yoksa kfree()'nin kendisi oops verir.

Detection

  • loop_rw_iter dosyalarını hedefleyen IOSQE_BUFFER_SELECT'li read'ler anormaldir; io_uring opcode/flag'lerini denetlemek bunu açığa çıkarır.
  • CONFIG_SLUB_DEBUG / slub_debug=FZ red-zoning, object dışındaki kfree offset'ini yakalar; KASAN UAF read/write'ı doğrudan işaretler.
  • Bloklanan FUSE read'leriyle eşleşen setxattr/getxattr patlamaları genel bir heap-spray işaretidir.

Mitigation

  • 5.14.7 / 5.10.x stable'da patch'lendi: loop_rw_iter() artık buffer-select request'leri için req->rw.addr'ı ilerletmez. Fix commit 16c8d2df7ec0eed31b7d3b61cb13206a7fb930cc ("io_uring: ensure symmetry in handling iter types in loop_rw_iter()") setup/advance simetrisini geri getirir — req->rw.addr/req->rw.len'i yalnızca bvec durumu için ayarlar ve iov_iter_advance()'i yalnızca non-bvec durumu için çağırır.
  • kernel.io_uring_disabled (6.6+) veya io_uring_setup'ın seccomp ile reddi surface'i ortadan kaldırır.
  • CONFIG_SLAB_FREELIST_HARDENED / _RANDOM ve dedicated buffer cache'ler kmalloc-32 reclaim'i için çıtayı yükseltir.

References