Userfaultfd

#race-conditions#toctou#userfaultfd#uaf
Vuln Classestoctou
Requirestoctou-window
Givesdeterministic-race-win
HardeningUFFD_USER_MODE_ONLY

userfaultfd is a Linux mechanism that has been widely used to reliably exploit race-condition vulnerabilities by pausing a victim thread at a carefully chosen point and allowing an attacker-controlled thread to manipulate the relevant state, particularly in TOCTOU vulnerabilities. However, since Linux 5.11, unprivileged processes are restricted by default from creating a userfaultfd capable of handling page faults that occur while the kernel accesses their memory. Instead, they can create a userfaultfd with the UFFD_USER_MODE_ONLY flag, which limits it to page faults generated by userspace execution. This prevents the classic technique of stalling kernel code at operations such as copy_from_user(). The restriction can be relaxed system-wide by an administrator through the vm.unprivileged_userfaultfd sysctl (echo 1 > /proc/sys/vm/unprivileged_userfaultfd), or bypassed by processes with the required privilege (e.g., CAP_SYS_PTRACE).


How It Works

This technique basically creates a fd mapped userfault handler. The attacker create a mmap page, register the page with the intented fault handler to the userfault fd and then force a fault trigger on this registered page from the kernel space. This way, the kernel thread that triggered the fault can be forced to obey whatever the fault handler is programmed to. Meanwhile, the attacker thread can win the race in TOCTOU and release the victim thread to continue. So, userfaultfd ultimately gives control over when a memory fault completes

  1. Create a userfaultfd FD
  2. Create a memory region with mmap()
  3. Register that memory region with userfaultfd
  4. Create a fault-handler function
  5. Start the fault handler in another thread
  6. Trigger a fault by accessing the registered, missing page
  7. Handler receives the fault event
  8. Resolve the fault

Example Code to Understand


void *handler(void *arg)
{
    int uffd = *(int *)arg;
    struct uffd_msg msg;
    read(uffd, &msg, sizeof(msg));
    printf("Handler: page fault!\n");
    printf("Fault address = %p\n",(void *)msg.arg.pagefault.address);

    char *src = mmap(NULL, PAGE_SIZE,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);

    strcpy(src, "Hello world");

    /* Give that page to the kernel */
    struct uffdio_copy copy = {
        .src = (unsigned long)src,
        .dst = msg.arg.pagefault.address & ~(PAGE_SIZE - 1),
        .len = PAGE_SIZE
    };
    ioctl(uffd, UFFDIO_COPY, &copy);
    return NULL;
}

int main()
{
    /* Create userfaultfd */
    int uffd = syscall(SYS_userfaultfd, O_CLOEXEC);
    /* Enable UFFD API */
    struct uffdio_api api = {
        .api = UFFD_API
    };
    ioctl(uffd, UFFDIO_API, &api);

    /* Allocate one page */
    char *page = mmap(NULL, PAGE_SIZE,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);

    /* Register the page */
    struct uffdio_register reg = {
        .range.start = (unsigned long)page,
        .range.len = PAGE_SIZE,
        .mode = UFFDIO_REGISTER_MODE_MISSING
    };

    ioctl(uffd, UFFDIO_REGISTER, &reg);

    /* THIS starts our handler */
    pthread_t thread;
    pthread_create(&thread, NULL, handler, &uffd);

    /* This causes the page fault */
    printf("Data = %s\n", page);
    // Or attacker forces the kernel space to trigger fault on the mmaped page.

    pthread_join(thread, NULL);

    return 0;
}

References