io_uring SQPOLL technique¶
IORING_SETUP_SQPOLL ile worker/thread affinity pinning'i birlikte kullanarak kernel-tarafı submission ve reclaim'in tek bir CPU üzerinde olmasını zorla; böylece free edilmiş bir object ve onun replacement'ı aynı SLUB per-CPU slab'ine düşer — güvenilmez bir cross-CPU UAF'ı güvenilir hale getirir.
Mechanism¶
Note
SLUB allocator per-CPU'dur: her CPU belirli bir cache (örneğin kmalloc-32) için aktif bir slab (kmem_cache_cpu.freelist) tutar. Bir object'i kfree() ettiğinde, o an hangi CPU'nun freelist'i geçerliyse ona geri döner; farklı bir CPU üzerindeki aynı boyutta bir sonraki kmalloc() farklı bir slab page'inden çeker. Yani bir use-after-free CPU A'da bir victim object'i free ederken senin reclaim spray'in CPU B'de çalışıyorsa, spray dangling object'e düşemez — slab'ler asla örtüşmez. Bu yüzden herhangi bir heap UAF'ın güvenilirliği free ve realloc'u tek bir CPU'da yan yana getirmeye bağlıdır.
io_uring yan yana getirmeyi zorlaştırır çünkü iş herhangi bir CPU'da çalışabilen io-wq worker kthread'lerine dağıtılır ve io_uring_enter(2) submission'ı caller'ın context'inde başka bir CPU'da çalışır. SQPOLL tekniği bu belirsizliği üç ayarla ortadan kaldırır:
IORING_SETUP_SQPOLL— ring setup'ında kernel özel bir submission-queue polling kthread'i (io_sq_thread) spawn eder. SQ ring'i busy-poll eder; userspace sadece bir SQE yazıp tail'i ilerleterek, hiçio_uring_entersyscall'ı olmadan submit eder. Submission bu yüzden her zaman keyfi syscall context'lerinde değil, tek SQPOLL kthread'inin context'inde çalışır.IORING_REGISTER_IOWQ_AFF— ring'in io-wq worker pool'u için bir CPU affinity mask register eder; blocking op'ları çalıştıran (victim'i free eden) worker'ları seçilmiş bir CPU'ya pinler.sched_setaffinity(2)— reclaim spray'i yapan exploit thread'lerini aynı CPU'ya pinler.
Üçü de hizalanınca, object free'si (pinli worker / SQPOLL thread tarafından yapılan) ve reclaim allocation'ı (pinli exploit thread tarafından yapılan) aynı per-CPU slab'den çeker; böylece spray free edilmiş slot'u güvenilir şekilde yeniden işgal eder. Bu CVE-2021-41073 exploit'inin arkasındaki etkinleştirici primitive'dir, ama free'nin worker context'inde olduğu herhangi bir io_uring-tabanlı UAF'a genelleşir.
Walkthrough¶
Bir SQPOLL ring kur ve her şeyi CPU 0'a pinle.
#include <liburing.h>
#include <sched.h>
struct io_uring ring;
struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL; /* spawn the SQ poller kthread */
p.sq_thread_idle = 2000; /* ms before the poller sleeps */
io_uring_queue_init_params(256, &ring, &p);
/* 1. pin io-wq workers (the ones that run blocking ops) to CPU 0 */
cpu_set_t aff; CPU_ZERO(&aff); CPU_SET(0, &aff);
io_uring_register_iowq_aff(&ring, sizeof(aff), &aff);
/* 2. pin THIS thread (the reclaim sprayer) to CPU 0 as well */
sched_setaffinity(0, sizeof(aff), &aff);
Syscall olmadan submit etmek (SQPOLL özelliği): bir SQE hazırla, sonra sadece tail'i ilerlet — poller onu alır.
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, iov, 1, 0);
io_uring_submit(&ring); /* with SQPOLL this only writes the ring tail; no enter(2) */
Warning
IORING_SETUP_SQPOLL, 5.11'den önceki kernel'larda CAP_SYS_ADMIN gerektirir. 5.11'den itibaren unprivileged bir user onu set edebilir. Worker'ın da uyanmasına ihtiyacın varsa, poller idle olmuş olabilir (sq_thread_idle geçmiştir) — bu durumda io_uring_submit gerçekten IORING_ENTER_SQ_WAKEUP ile bir io_uring_enter çıkarır ve anlık olarak "syscall yok" özelliğini bozar. Poller'ı sıcak tutmak için trafiği akar tut.
Affinity'nin gerçekten etki ettiğini worker kthread'inin CPU'sunu okuyarak doğrula:
# the io_uring poller / workers appear as iou-sqp-<pid> and iou-wrk-<pid>
$ ps -eLo pid,tid,psr,comm | grep iou-
1337 1338 0 iou-sqp-1337 # psr column == 0 -> running on CPU 0
1337 1339 0 iou-wrk-1337
Free ve realloc'un bir slab paylaştığını, hedef cache için per-CPU freelist sayısını spray'den önce ve sonra izleyerek doğrula:
Aynı-CPU slab reuse'unu gözlemleme (kmalloc-32)
$ cat /sys/kernel/slab/kmalloc-32/cpu_slabs
1 N0=1 # one active per-cpu slab on node 0
# trigger the UAF free (pinned worker), then spray N reclaim objects
# pinned to the same CPU. If co-location worked, the freed slot is
# immediately handed back to the very next same-size alloc:
# free(victim) -> kmem_cache_cpu.freelist head = victim
# kmalloc(32) -> returns victim <-- overlap achieved
Registered-file açısı: SQPOLL yaygın olarak registered (fixed) file'larla (IORING_REGISTER_FILES) eşleştirilir; burada ring struct file *'ı ctx->file_table'da cache'ler. Bir UAF, bir registered file'ın işaret ettiği slot'u reclaim etmene izin verirse, tamamen kernel-tarafı poller tarafından sürülen bir struct file type confusion elde edersin — bu yüzden "SQPOLL ile registered file UAF" takma adı.
Detection¶
- Pinli bir CPU'da %100'de dönen olağandışı
iou-sqp-*/iou-wrk-*kthread'leri (SQPOLL poller busy-poll yapar)top/ps'te gözlemlenebilir. IORING_SETUP_SQPOLLileio_uring_setup(2)'yi veIORING_REGISTER_IOWQ_AFFileio_uring_register(2)'yiseccomp/auditüzerinden denetlemek exploit-ilgili konfigürasyonu işaretler.- SLUB debug (
slub_debug=FZ) veyaCONFIG_SLAB_FREELIST_HARDENED, bu tekniğin dayandığı deterministik freelist reuse'unu bozar.
Mitigation¶
- io_uring'i tamamen devre dışı bırakmak (6.6'da eklenen
sysctl kernel.io_uring_disabled=2) primitive'i ortadan kaldırır. io_uring_setup'ı kısıtlayankernel.io_uring_group/ Landlock / seccomp, güvenilmeyen koda SQPOLL'u reddeder.CONFIG_SLAB_FREELIST_RANDOMve per-CPU freelist hardening, "free edilmiş slot bir sonraki alloc'a döner" determinizmini azaltır, yan yana getirme garantisini zayıflatır (ortadan kaldırmaz).