Skip to content

mq_notify netlink sock double sock_put UAF (CVE-2017-11176)

sys_mq_notify() retry path'inde eksik bir sock = NULL, cleanup kodunun zaten serbest bırakılmış bir netlink socket üzerinde sock_put()'ı ikinci kez çağırmasına yol açar; bu da refcount'u erkenden sıfıra düşürür ve geride reclaim edilip yeniden kullanılacak dangling bir struct sock bırakır.

Mechanism

mq_notify(2), bir process'in boş bir POSIX queue'ya mesaj geldiğinde (bir signal ya da bir netlink mesajı üzerinden) haberdar edilmek için kayıt olmasını sağlar. Notification bir netlink socket üzerinden teslim edildiğinde, syscall ona bir control skb attach ederken o socket'i pin'lemek zorundadır. Bug, error/retry path'indeki bir reference-count dengesizliğidir: bir reference iki kez drop edilir.

Note

Invariant her zamanki gibidir — her sock_hold() tam olarak bir sock_put() ile eşleşmeli ve reference'ı drop edilmiş bir pointer yeniden kullanılmamalıdır. Açık barındıran sys_mq_notify() şöyle görünür (Lexfo, 4.11.9'a kadarki kernel'ler):

retry:
    filp = fget(notification.sigev_signo);
    if (!filp) { ret = -EBADF; goto out; }

    sock = netlink_getsockbyfilp(filp);   /* does sock_hold(sock) */
    fput(filp);
    if (IS_ERR(sock)) { ret = PTR_ERR(sock); sock = NULL; goto out; }

    ret = netlink_attachskb(sock, nc, &timeo, NULL);
    if (ret == 1)
        goto retry;                       /* BUG: sock still set, ref already dropped */
    if (ret) { ... goto out; }
...
out:
    ...
    if (sock)
        netlink_detachskb(sock, nc);      /* second sock_put() on a stale sock */

Reference muhasebesi:

  • netlink_getsockbyfilp(), sock_hold() ile bir reference alır.
  • netlink_attachskb(), socket'in receive buffer'ı dolu olup sleep/wake yapmak zorunda kaldığında 1 döner — ve o return path'inde reference'ı zaten drop eder (sock_put()), caller'ın retry'da yeniden almasını bekler.
  • Açık barındıran kod, sock'u temizlemeden goto retry yapar. Eğer ardından fget() başarısız olursa (örneğin attacker iterasyonlar arasında fd'yi kapatırsa), kontrol artık reference'sız olan socket'i hâlâ gösteren sock ile out:'a atlar ve netlink_detachskb() ikinci bir sock_put() gerçekleştirir.

Bir sock_hold()'a karşılık iki sock_put(), refcount'u bir adım erken sıfıra düşürür; struct sock, meşru bir kullanıcı hâlâ onu tutarken free edilir. Sonuç, bir netlink struct sock üzerinde klasik bir use-after-free / dangling-pointer'dır (bu bir refcount-imbalance-uaf'tir). Resmî fix tek satırdır:

    if (ret == 1) {
+       sock = NULL;
        goto retry;
    }

sock'u temizlemek, out:'taki if (sock) netlink_detachskb(...)'ı retry'da bir no-op hâline getirir ve 1:1 dengesini geri kurar.

Walkthrough

Bug'a tamamen unprivileged userspace'ten ulaşılır; zor kısım, netlink_attachskb()'yi 1 döndürmeye zorlayan ve sonra bir sonraki fget()'i başarısız kılan race'i kazanmaktır.

  1. Bir netlink socket oluştur ve receive buffer'ını doldur ki netlink_attachskb() block etsin ve 0 yerine retry değeri 1'i döndürsün. Lexfo bunu, küçücük bir SO_RCVBUF ayarlayıp socket'i flood ederek yapar; böylece bir sonraki attach sleep etmek zorunda kalır.

  2. Notification'ı bir POSIX mqueue üzerine register et, sigev_signo'yu netlink socket fd'sine yönlendir:

struct sigevent sigev = {0};
sigev.sigev_notify = SIGEV_THREAD;       /* netlink-backed delivery */
sigev.sigev_signo  = netlink_fd;         /* fd of the full netlink sock */
/* ...sigev_value carries the netlink cookie... */
mq_notify(mqdes, &sigev);                /* enters the vulnerable retry loop */
  1. İkinci bir thread'den, birincisi netlink_attachskb() içinde park hâlindeyken (ki 1 dönecektir), netlink_fd'yi kapat ki goto retry sonrasındaki fget() -EBADF ile başarısız olsun. Bu, ilk thread'i stale bir sock ile out:'a gönderir ve ikinci sock_put()'ı tetikler.

!!! warning Bu sıkı bir race'tir. Lexfo bunu, scheduler'a duyarlı pencereyi single-step'leyerek (attach yapan thread'in ne zaman uyanacağını kontrol ederek) ve netlink buffer üzerinde bir blocking-then-unblock pattern kullanarak stabilize eder. Bu kontrol olmadan ikinci fget() genelde başarılı olur ve bug ateşlenmez. Trigger'ın kendisi değil, reliability işi exploit'in büyük kısmını oluşturur.

  1. Free edilmiş struct sock artık dangling'dir. Aynı kmalloc boyut ve şekline sahip kontrol edilebilir bir object ile slab slot'unu reclaim et (Lexfo socket'in cache'ini groom eder ve forged bir sock/netlink_sock overlay'ler), sonra dangling pointer üzerinden bir operation sürerek bir arbitrary-call primitive elde et ve oradan ring-0 code execution'a geç. Bu size class için pratik spray object'leri arasında mqueue mesajları yer alır — bkz. mqueue-object-spray ve setxattr-userfaultfd-universal-heap-spray.

??? example "Refcount timeline (one hold, two puts)"

netlink_getsockbyfilp()  : sock_hold()   refcount 1 -> 2
netlink_attachskb() == 1 : sock_put()    refcount 2 -> 1   (buffer full, retry)
goto retry; fget() fails : (sock NOT cleared)
out: netlink_detachskb() : sock_put()    refcount 1 -> 0   -> FREED
...later legitimate use  : use-after-free on struct sock

Detection

  • Fix, tek satırlık bir refcount düzeltmesidir; build/patch scanner'ları ipc/mqueue.c:sys_mq_notify içinde goto retry'dan önce sock = NULL; varlığını anahtar olarak kullanabilir.
  • Runtime'da, CONFIG_REFCOUNT_FULL / saturating refcount_t dönüşümü, erken drop-to-zero'yu ve ardından gelen inc-from-zero'yu bir WARN'a çevirir ve sessizce free etmek yerine dengesizliği yüzeye çıkarır.
  • KASAN, netlink struct sock slab object'i üzerindeki use-after-free read/write'ı işaretler.

Mitigation

  • Upstream fix'i uygula (ret == 1 retry path'ine sock = NULL; ekleyen commit); bakımı yapılan tüm distro kernel'leri bunu 2017 ortasından beri taşır.
  • Generic hardening, ortaya çıkan UAF'ın exploitability'sini azaltır: SLAB_FREELIST_RANDOM/HARDENED, init_on_free ve refcount saturation (refcount_t), dangling-sock yeniden kullanımını çok daha az deterministik kılar.
  • Sınıf fix'i, el yapımı atomic_t socket refcounting'i checked refcount_t ile değiştirmektir; bu, off-by-one'ı bir free yerine tespit edilebilir bir saturation'a çevirir.

References