Skip to content

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_enter syscall'ı 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_SQPOLL ile io_uring_setup(2)'yi ve IORING_REGISTER_IOWQ_AFF ile io_uring_register(2)'yi seccomp/audit üzerinden denetlemek exploit-ilgili konfigürasyonu işaretler.
  • SLUB debug (slub_debug=FZ) veya CONFIG_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ıtlayan kernel.io_uring_group / Landlock / seccomp, güvenilmeyen koda SQPOLL'u reddeder.
  • CONFIG_SLAB_FREELIST_RANDOM ve 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).

References