A use-after-free can occur in kvm_xen_shared_info_init() when caching user virtual addresses (HVAs) that are not backed by a KVM memslot: BUG: KASAN: use-after-free in kvm_xen_shared_info_init+0x35b/0x590 Read of size 4 at addr ffff8881f1b77900 by task syz-executor675/5843 Call Trace: kasan_report+0x117/0x150 mm/kasan/report.c:595 kvm_xen_shared_info_init+0x35b/0x590 arch/x86/kvm/xen.c:90 kvm_xen_hvm_set_attr+0x81a/0x1750 arch/x86/kvm/xen.c:806 kvm_arch_vm_ioctl+0xdc7/0x1a00 arch/x86/kvm/x86.c:7514 kvm_vm_ioctl+0x8f7/0xd30 virt/kvm/kvm_main.c:5381 ... page last free pid 5843 tgid 5842 stack trace: __free_frozen_pages+0xbdb/0xcb0 mm/page_alloc.c:2950 __folio_put+0x2b5/0x350 mm/swap.c:112 hva_to_pfn_retry virt/kvm/pfncache.c:246 [inline] __kvm_gpc_refresh+0x1274/0x1710 virt/kvm/pfncache.c:330 __kvm_gpc_activate+0x286/0x3b0 virt/kvm/pfncache.c:424 kvm_xen_hvm_set_attr+0x76f/0x1750 arch/x86/kvm/xen.c:798 ... The root cause is a flaw in how KVM's MMU notifiers interact with the gfn_to_pfn_cache (gpc). When a user virtual address is not backed by a memslot (e.g., when KVM_XEN_HVM_SET_ATTR maps a Xen shared info page using an anonymous mmap region), the MMU notifier invalidation logic skips calling kvm_mmu_invalidate_end() because it finds no intersecting memslots. Consequently, kvm->mmu_invalidate_seq is never incremented for this invalidation. Concurrently, if hva_to_pfn_retry() is refreshing the gpc, it drops the gpc->lock to call hva_to_pfn(). If a concurrent MADV_DONTNEED unmaps the page and finishes invalidate_range_end() while the lock is dropped, hva_to_pfn_retry() will fail to detect the invalidation because mmu_invalidate_seq was not incremented. It will then incorrectly cache and use a freed page, leading to a use-after-free. To fix this without introducing unconditional mmu_lock contention for gpc-only invalidations, introduce a dedicated sequence counter, gpc_invalidate_seq, for the gfn_to_pfn_cache. Increment this counter in gfn_to_pfn_cache_invalidate_start() whenever an invalidated HVA range overlaps with any gpc->uhva, even if the gpc is currently marked as invalid. This ensures that hva_to_pfn_retry() can detect concurrent invalidations by snapshotting and checking gpc_invalidate_seq alongside mmu_invalidate_seq. Because hva_to_pfn_retry() also checks kvm->mn_active_invalidate_count (which is correctly incremented and decremented unconditionally by the MMU notifier hooks), incrementing the sequence counter at the start of the invalidation is perfectly safe. If the invalidation is still in progress when hva_to_pfn_retry() re-acquires the lock, mn_active_invalidate_count > 0 will force a retry anyway. If the invalidation has finished, the changed gpc_invalidate_seq will force the retry. Fixes: 721f5b0dda78 ("KVM: pfncache: allow a cache to be activated with a fixed (userspace) HVA") Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot Reported-by: syzbot+0948c82180d475ad24e2@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=0948c82180d475ad24e2 Link: https://syzkaller.appspot.com/ai_job?id=f8623867-bd64-456f-b512-82c9a04c8460 To: To: "Paolo Bonzini" To: "Paul Durrant" Cc: --- diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h index ab8cfaec8..4c0970855 100644 --- a/include/linux/kvm_host.h +++ b/include/linux/kvm_host.h @@ -805,6 +805,7 @@ struct kvm { /* For management / invalidation of gfn_to_pfn_caches */ spinlock_t gpc_lock; struct list_head gpc_list; + unsigned long gpc_invalidate_seq; /* * created_vcpus is protected by kvm->lock, and is incremented diff --git a/virt/kvm/pfncache.c b/virt/kvm/pfncache.c index 728d2c1b4..877d6d1d8 100644 --- a/virt/kvm/pfncache.c +++ b/virt/kvm/pfncache.c @@ -26,34 +26,40 @@ void gfn_to_pfn_cache_invalidate_start(struct kvm *kvm, unsigned long start, unsigned long end) { struct gfn_to_pfn_cache *gpc; + bool wake = false; spin_lock(&kvm->gpc_lock); list_for_each_entry(gpc, &kvm->gpc_list, list) { read_lock_irq(&gpc->lock); /* Only a single page so no need to care about length */ - if (gpc->valid && !is_error_noslot_pfn(gpc->pfn) && - gpc->uhva >= start && gpc->uhva < end) { - read_unlock_irq(&gpc->lock); - - /* - * There is a small window here where the cache could - * be modified, and invalidation would no longer be - * necessary. Hence check again whether invalidation - * is still necessary once the write lock has been - * acquired. - */ - - write_lock_irq(&gpc->lock); - if (gpc->valid && !is_error_noslot_pfn(gpc->pfn) && - gpc->uhva >= start && gpc->uhva < end) - gpc->valid = false; - write_unlock_irq(&gpc->lock); - continue; + if (gpc->uhva >= start && gpc->uhva < end) { + wake = true; + + if (gpc->valid && !is_error_noslot_pfn(gpc->pfn)) { + read_unlock_irq(&gpc->lock); + + /* + * There is a small window here where the cache could + * be modified, and invalidation would no longer be + * necessary. Hence check again whether invalidation + * is still necessary once the write lock has been + * acquired. + */ + + write_lock_irq(&gpc->lock); + if (gpc->valid && !is_error_noslot_pfn(gpc->pfn) && + gpc->uhva >= start && gpc->uhva < end) + gpc->valid = false; + write_unlock_irq(&gpc->lock); + continue; + } } read_unlock_irq(&gpc->lock); } + if (wake) + kvm->gpc_invalidate_seq++; spin_unlock(&kvm->gpc_lock); } @@ -124,7 +130,8 @@ static void gpc_unmap(kvm_pfn_t pfn, void *khva) #endif } -static inline bool mmu_notifier_retry_cache(struct kvm *kvm, unsigned long mmu_seq) +static inline bool mmu_notifier_retry_cache(struct kvm *kvm, unsigned long mmu_seq, + unsigned long gpc_seq) { /* * mn_active_invalidate_count acts for all intents and purposes @@ -149,7 +156,7 @@ static inline bool mmu_notifier_retry_cache(struct kvm *kvm, unsigned long mmu_s * new (incremented) value of mmu_invalidate_seq is observed. */ smp_rmb(); - return kvm->mmu_invalidate_seq != mmu_seq; + return kvm->mmu_invalidate_seq != mmu_seq || kvm->gpc_invalidate_seq != gpc_seq; } static kvm_pfn_t hva_to_pfn_retry(struct gfn_to_pfn_cache *gpc) @@ -159,6 +166,7 @@ static kvm_pfn_t hva_to_pfn_retry(struct gfn_to_pfn_cache *gpc) kvm_pfn_t new_pfn = KVM_PFN_ERR_FAULT; void *new_khva = NULL; unsigned long mmu_seq; + unsigned long gpc_seq; struct page *page; struct kvm_follow_pfn kfp = { @@ -182,6 +190,7 @@ static kvm_pfn_t hva_to_pfn_retry(struct gfn_to_pfn_cache *gpc) do { mmu_seq = gpc->kvm->mmu_invalidate_seq; + gpc_seq = gpc->kvm->gpc_invalidate_seq; smp_rmb(); write_unlock_irq(&gpc->lock); @@ -232,7 +241,7 @@ static kvm_pfn_t hva_to_pfn_retry(struct gfn_to_pfn_cache *gpc) * attempting to refresh. */ WARN_ON_ONCE(gpc->valid); - } while (mmu_notifier_retry_cache(gpc->kvm, mmu_seq)); + } while (mmu_notifier_retry_cache(gpc->kvm, mmu_seq, gpc_seq)); gpc->valid = true; gpc->pfn = new_pfn; base-commit: 075b74841bd0065a3bda3440873c747938e69b68 -- This is an AI-generated patch subject to moderation. Reply with '#syz upstream' to Sign-off the patch as a human author and send it to the upstream kernel mailing lists. Reply with '#syz reject' to reject it ('#syz unreject' to undo). See https://goo.gle/syzbot-ai-patches for information about AI-generated patches. You can comment on the patch as usual, syzbot will try to address the comments and send a new version of the patch if necessary. syzbot engineers can be reached at syzkaller@googlegroups.com.