Skip to content

Packet socket race-within-a-race (CVE-2025-38617)

AF_PACKET'te packet_set_ring() ile packet_notifier() arasında, po->num'ı koşullu sıfırlamaya dayanan bir race; ring buffer üzerinde bir use-after-free verir ki unprivileged bir kullanıcı (userns üzerinden CAP_NET_RAW) bunu tam bir privilege escalation'a zincirleyebilir.

Mechanism

Note

packet_set_ring() bir ring'i tear down ettiğinde veya reconfigure ettiğinde, tüm critical section boyunca socket'i packet delivery'den unhooked tutmalıdır. Orijinal kod, po->num'ı (bound protocol number) yalnızca socket zaten çalışıyorken sıfırlardı:

if (was_running) {
    WRITE_ONCE(po->num, 0);
    __unregister_prot_hook(sk, false);
}

Bu noktada interface down idiyse, po->num non-zero değerini korur. packet_set_ring(), po->bind_lock'u bıraktıktan sonra, eşzamanlı bir NETDEV_UP event'i packet_notifier()'ı çalıştırır, non-zero bir po->num görür ve register_prot_hook()'u çağırır — ring free edilirken/reconfigure edilirken socket'i delivery için yeniden hook'lar. Packet'ler sonra tpacket_rcv()'a ulaşır; o da packet_set_ring()'in eşzamanlı free ettiği bir ring buffer'a yazar: page-level bir use-after-free.

Bug, Linux 2.6.12'ye (2005) dayanır ve 6.16'da düzeltildi.

Walkthrough

"A race within a race", public exploit'in (Quang Le / Calif, Google kernelCTF) zincirlediği iki iç içe pencereyi tanımlar:

  1. Outer racepacket_set_ring()'in po->bind_lock'u bırakması ile sonraki lock'u yeniden alması arasında, po->num sıfırlanmadığı için packet_notifier() socket'i yeniden hook'lar.
  2. Inner race — socket yeniden hook'lanmışken, gelen bir packet tpacket_rcv()'ı tam olarak packet_set_ring() ring'i free ettiği anda ring'e yazmaya sürer.

Nanosaniyelik bir pencereyi kullanılabilir hale getirmek için exploit onu genişletir (barrier-tarzı bir trick): bir ring-buffer lock holder'ı uyut ki "lock release ile sonraki acquisition arasındaki süre öngörülebilir ve esnetilebilir hale gelsin" — örn. SO_SNDTIMEO üzerinden tpacket_snd()'i ~1s'lik bir wait_for_completion_interruptible_timeout() boyunca po->pg_vec_lock'u tutmaya zorlayarak.

Writeup'ta raporlanan aşamalar
  • Stage 0 — pencereyi genişlet sleeping-lock-holder barrier'ı ile.
  • Stage 1 — heap overflow: free edilen ring'i daha küçük block'larla reclaim et; stale metadata, tpacket_rcv()'ın bir block'u aşıp yazmasına neden olur; bitişik bir simple_xattr size field'ına 8-byte'lık bir write konar (8192 → 65536) ve oversized bir read/write oluşur.
  • Stage 2/3 — pgv overlap: bir pgv array girdisini bir kernel address'i ile overwrite et ve iki overlapping ring buffer inşa et (block'ları bir "puppet" buffer'ın pgv'sini overlay eden bir "master"); mmap'lenen ring üzerinden arbitrary page-aligned kernel R/W verir.
  • Stage 4 — KASLR bypass: bir pipe buffer array'i leak'le, anon_pipe_buf_ops'u bul, kernel base'i türet.
  • Stage 5 — privesc: __do_sys_kcmp() handler'ını, caller'ın cred'lerini init_cred'e ve fs'ini init_fs'e swap eden shellcode ile overwrite et.

Warning

Exploit modern hardening'i açıkça yener: CONFIG_RANDOM_KMALLOC_CACHES bypass edilir çünkü her iki pgv array'i de alloc_pg_vec() içindeki aynı kcalloc()'tan gelir (özdeş cache hash); ve CONFIG_SLAB_VIRTUAL, same-type reclamation (ring buffer'ların ring buffer'ları reclaim etmesi) kullanılarak bypass edilir, böylece virtual-address aralıkları tutarlı kalır.

Detection

NETDEV_UP churn'ü ile eşzamanlı ring reconfiguration, namespaced unprivileged context'lerden AF_PACKET kullanımı ve SO_SNDTIMEO + blocking tpacket_snd() pattern'leri anomalidir.

Mitigation

  • Upstream'de (commit 01d3c8417b9c) state update'i unconditional yaparak düzeltildi — was_running check'inden önce her zaman WRITE_ONCE(po->num, 0) — böylece packet_notifier() her zaman po->num == 0 görür ve pencere sırasında yeniden hook'layamaz.
  • CAP_NET_RAW entrypoint'ini reddetmek için unprivileged user namespace'leri engelle; kernel'leri >= 6.16 tut.

References