Skip to content

io_uring vulnerability (CVE-2022-29582)

Linkli io_uring timeout'ları arasındaki bir concurrency bug'ı, io_link_timeout_fn()'in linkli request free edildikten sonra bir link'i yerinde bırakmasına izin verir; bu da bir io_kiocb üzerinde use-after-free verir, bir struct file refcount düşürmesine ve cross-cache üzerinden tam LPE'ye yükseltilir.

Mechanism

Note

io_uring, request'lerin linklenmesine izin verir: bir IORING_OP_TIMEOUT (T) bir zinciri koruyabilir ve bir IORING_OP_LINK_TIMEOUT (LT) önceki bir request'in ne kadar çalışabileceğini sınırlar. Her io_kiocb reference count'lanır (req->refs). Timeout mekanizmasının korumak zorunda olduğu invariant: LT'nin timer'ı ateşlendiğinde, LT'yi link list'inden ancak önceki request'in hâlâ canlı olduğunu atomik olarak doğruladıktan sonra çıkarır. io_link_timeout_fn()'deki zafiyetli kod bu iki adımı yanlış sırada yapıyordu:

/* vulnerable ordering (pre-5.17.3) */
if (refcount_inc_not_zero(&prev->refs))
    list_del_init(&req->link_list);   /* only unlinked if prev still alive */

LT'nin timer'ı ateşlendiğinde T'nin refcount'u zaten sıfıra ulaşmışsa, refcount_inc_not_zero() false döner ve list_del_init() atlanır — LT, zaten yok edilme yolunda olan bir request'e linkli kalır. Ertelenmiş completion worker'ları (io_put_req_deferred_cb) sonra sırasız çalışır ve T'nin teardown'u free edilmiş LT io_kiocb'sine dokunur. İşte bu use-after-free'dir. Pratikte race io-wq worker'ları boyunca koşar (Black Hat US-23 "Bad io_uring" sunumunda green/purple/red worker'lar olarak adlandırılır): bir worker T'nin son reference'ını düşürürken bir diğeri hâlâ LT'yi link_list üzerinden dereference eder. Her iki object da aynı cache'ten allocate edilmiş io_kiocb'ler olduğundan, race bug'ın tamamıdır; sonrasındaki her şey dar bir pencereyi deterministik bir primitive'e şekillendirmektir. 5.17.3 / 5.10.109'da e677edbcabee commit'iyle fix edildi; bu commit önce koşulsuz unlink yapar, sonra prev gitmişse onu null'lar.

Walkthrough

Ruia-Ruia ("Reviving Exploits Against Cred Structs") yazısını izleyerek:

  1. Race'i kazan / free edilmiş object'i stabilize et. Pencere dar, bu yüzden free edilmiş LT slot'unu boş bırakmak yerine onu bir IORING_OP_TEE request'iyle (LT') reclaim et. do_tee(), verisi olmayan bir pipe'tan tee yaparak süresiz blok yapacak şekilde ayarlanabilir; LT'yi free edilmiş slot'a park eder ve exploit'i açık tutar.

  2. UAF → file refcount bug'ına çevir. T'nin yok etme path'i io_kill_linked_timeout() çağırır; bu LT'yi canlı bir timeout olarak tanımaz, bu yüzden LT' io_fail_links() üzerinden teardown edilir. IORING_OP_TEE, input dosyasına req->splice.file_in'de bir reference tutar; LT'yi teardown etmek o reference'ı düşürür. Pipe'ın struct fileuserspace hâlâ fd'yi tutarken free edilir — dangling bir file.

  3. filp slab'inden cross-cache ile kaç. file cache'i type-isolated'dır, bu yüzden yazarlar slot'u yerinde yeniden kullanmaz. Slab'i tüketir, per-CPU partial list'i taşırarak unfreeze_partials()'ı zorlar, artık boş olan page'i buddy allocator'a geri verir ve kmalloc-512'ye yeniden devredilmesini sağlar.

  4. Message segment olarak yeniden spray. Page'i msgsnd() üzerinden msg_msgseg object'leriyle reclaim et, bir message body'sinin içine sahte bir file yapısı göm, sonra dangling fd'yi close() ederek onu tekrar free et — kontrol edilebilir bir msg_msgseg UAF üretir.

  5. KASLR sızdır. Free edilmiş msg_msgseg slot'larına tls_context object'leri (bir TCP socket'ini kTLS'e yükselterek oluşturulan) spray'le; tls_context'in içindeki tcp_prot pointer'ı kernel base'ini açığa çıkarır.

  6. Code execution. Socket'in sk_proto->getsockopt()'unu bir stack-pivot gadget'ıyla overwrite et ve getsockopt() çağırarak bir ROP zinciri sür.

/* shape of the linked submission that arms the bug */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_timeout(sqe, &ts_long, 0, 0);
sqe->flags |= IOSQE_IO_LINK;          /* this is T (the parent timeout) */

sqe = io_uring_get_sqe(&ring);
io_uring_prep_link_timeout(sqe, &ts_short, 0);  /* this is LT */
io_uring_submit(&ring);
/* race T's completion against LT's timer firing -> dangling io_kiocb */
Fix (commit e677edbcabee)
/* fixed ordering: unlink unconditionally, THEN test liveness */
list_del_init(&req->link_list);
if (!refcount_inc_not_zero(&prev->refs))
    prev = NULL;

Warning

Bu, çok küçük bir pencereye sahip gerçek bir race — CVE metni bile bunun "belki yalnızca seyrek exploit edilebileceğini" not eder. Tüm exploit, kernel onu yeniden kullanamadan önce free edilmiş slot'u bloklayan bir IORING_OP_TEE ile park etmeye bağlıdır; bu olmadan UAF deterministik değildir. Not: bu bir Linux io_uring bug'ıdır ve bu KB'de yanında durduğu macOS/iOS maddeleriyle ilgisizdir.

Detection

  • KASAN, io_kiocb use-after-free'sini ve sonraki file/msg_msgseg reuse'unu doğrudan işaretler.
  • Birbirine karşı race'lenen linkli IORING_OP_TIMEOUT + IORING_OP_LINK_TIMEOUT submission'ları, ardından bir pipe üzerinde bloklayan bir IORING_OP_TEE, denetlemeye değer olağandışı bir opcode imzasıdır.
  • Cross-cache işaretleri: bir io_uring teardown'undan hemen sonra msgsnd() taşkınları ve TLS-yükseltme (setsockopt(..., TCP_ULP, "tls")) patlamaları.

Mitigation

  • 5.17.3'te ve 5.10.109+ / 5.15.x stable ağaçlarında patch'lendi (commit e677edbcabee).
  • kernel.io_uring_disabled (6.6+) veya io_uring_setup'ı reddeden bir seccomp filtresi surface'i tamamen ortadan kaldırır.
  • CONFIG_SLAB_FREELIST_HARDENED / _RANDOM ve type-isolated file cache'i çıtayı yükseltir; bu exploit özellikle filp izolasyonunun etrafından bir page-level cross-cache ile dolaşır.

References