From 5ff608da797cb293d172082b5f4fffbcbd9b55a2 Mon Sep 17 00:00:00 2001 From: Andrew Lukoshko Date: Thu, 2 Jul 2026 11:44:09 +0000 Subject: [PATCH] Add KVM x86 shadow paging use-after-free fixes ahead of RHEL; bump to 687.20.2 --- ...use-after-free-due-to-unexpected-gfn.patch | 134 ++++++++++++++++++ ...se-after-free-due-to-unexpected-role.patch | 67 +++++++++ SPECS/kernel.spec | 10 +- 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 SOURCES/1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch create mode 100644 SOURCES/1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch diff --git a/SOURCES/1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch b/SOURCES/1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch new file mode 100644 index 000000000..369c83b23 --- /dev/null +++ b/SOURCES/1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch @@ -0,0 +1,134 @@ +From 0cb2af2ea66ad8ff195c156ea690f11216285bdf Mon Sep 17 00:00:00 2001 +From: Sean Christopherson +Date: Wed, 15 Apr 2026 00:52:34 +0000 +Subject: [PATCH] KVM: x86: Fix shadow paging use-after-free due to unexpected + GFN + +The shadow MMU computes GFNs for direct shadow pages using sp->gfn plus +the SPTE index. This assumption breaks for shadow paging if the guest +page tables are modified between VM entries (similar to commit +aad885e77496, "KVM: x86/mmu: Drop/zap existing present SPTE even +when creating an MMIO SPTE", 2026-03-27). The flow is as follows: + +- a PDE is installed for a 2MB mapping, and a page in that area is + accessed. KVM creates a kvm_mmu_page consisting of 512 4KB pages; + the kvm_mmu_page is marked by FNAME(fetch) as direct-mapped because + the guest's mapping is a huge page (and thus contiguous). + +- the PDE mapping is changed from outside the guest. + +- the guest accesses another page in the same 2MB area. KVM installs + a new leaf SPTE and rmap entry; the SPTE uses the "correct" GFN + (i.e. based on the new mapping, as changed in the previous step) but + that GFN is outside of the [sp->gfn, sp->gfn + 511] range; therefore + the rmap entry cannot be found and removed when the kvm_mmu_page + is zapped. + +- the memslot that covers the first 2MB mapping is deleted, and the + kvm_mmu_page for the now-invalid GPA is zapped. However, rmap_remove() + only looks at the [sp->gfn, sp->gfn + 511] range established in step 1, + and fails to find the rmap entry that was recorded by step 3. + +- any operation that causes an rmap walk for the same page accessed + by step 3 then walks a stale rmap and dereferences a freed kvm_mmu_page. + This includes dirty logging or MMU notifier invalidations (e.g., from + MADV_DONTNEED). + +The underlying issue is that KVM's walking of shadow PTEs assumes that +if a SPTE is present when KVM wants to install a non-leaf SPTE, then the +existing kvm_mmu_page must be for the correct gfn. Because the only way +for the gfn to be wrong is if KVM messed up and failed to zap a SPTE... +which shouldn't happen, but *actually* only happens in response to a +guest write. + +That bug dates back literally forever, as even the first version of KVM +assumes that the GFN matches and walks into the "wrong" shadow page. +However, that was only an imprecision until 2032a93d66fa ("KVM: MMU: +Don't allocate gfns page for direct mmu pages") came along. + +Fix it by checking for a target gfn mismatch and zapping the existing +SPTE. That way the old SP and rmap entries are gone, KVM installs +the rmap in the right location, and everyone is happy. + +Fixes: 2032a93d66fa ("KVM: MMU: Don't allocate gfns page for direct mmu pages") +Fixes: 6aa8b732ca01 ("kvm: userspace interface") +Reported-by: Alexander Bulekov +Reported-by: Fred Griffoul +Cc: stable@vger.kernel.org +Signed-off-by: Sean Christopherson +Link: https://patch.msgid.link/20260503201029.106481-1-pbonzini@redhat.com/ +Signed-off-by: Paolo Bonzini +--- + arch/x86/kvm/mmu/mmu.c | 35 ++++++++++++++--------------------- + 1 file changed, 14 insertions(+), 21 deletions(-) + +diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c +index 8895c98..6140702 100644 +--- a/arch/x86/kvm/mmu/mmu.c ++++ b/arch/x86/kvm/mmu/mmu.c +@@ -183,6 +183,8 @@ struct kmem_cache *mmu_page_header_cache; + static struct percpu_counter kvm_total_used_mmu_pages; + + static void mmu_spte_set(u64 *sptep, u64 spte); ++static int mmu_page_zap_pte(struct kvm *kvm, struct kvm_mmu_page *sp, ++ u64 *spte, struct list_head *invalid_list); + + struct kvm_mmu_role_regs { + const unsigned long cr0; +@@ -1220,19 +1222,6 @@ static void drop_spte(struct kvm *kvm, u64 *sptep) + rmap_remove(kvm, sptep); + } + +-static void drop_large_spte(struct kvm *kvm, u64 *sptep, bool flush) +-{ +- struct kvm_mmu_page *sp; +- +- sp = sptep_to_sp(sptep); +- WARN_ON_ONCE(sp->role.level == PG_LEVEL_4K); +- +- drop_spte(kvm, sptep); +- +- if (flush) +- kvm_flush_remote_tlbs_sptep(kvm, sptep); +-} +- + /* + * Write-protect on the specified @sptep, @pt_protect indicates whether + * spte write-protection is caused by protecting shadow page table. +@@ -2328,7 +2317,8 @@ static struct kvm_mmu_page *kvm_mmu_get_child_sp(struct kvm_vcpu *vcpu, + { + union kvm_mmu_page_role role; + +- if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep)) ++ if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep) && ++ spte_to_child_sp(*sptep) && spte_to_child_sp(*sptep)->gfn == gfn) + return ERR_PTR(-EEXIST); + + role = kvm_mmu_child_role(sptep, direct, access); +@@ -2406,13 +2396,16 @@ static void __link_shadow_page(struct kvm *kvm, + + BUILD_BUG_ON(VMX_EPT_WRITABLE_MASK != PT_WRITABLE_MASK); + +- /* +- * If an SPTE is present already, it must be a leaf and therefore +- * a large one. Drop it, and flush the TLB if needed, before +- * installing sp. +- */ +- if (is_shadow_present_pte(*sptep)) +- drop_large_spte(kvm, sptep, flush); ++ if (is_shadow_present_pte(*sptep)) { ++ struct kvm_mmu_page *parent_sp; ++ LIST_HEAD(invalid_list); ++ ++ parent_sp = sptep_to_sp(sptep); ++ WARN_ON_ONCE(parent_sp->role.level == PG_LEVEL_4K); ++ ++ mmu_page_zap_pte(kvm, parent_sp, sptep, &invalid_list); ++ kvm_mmu_remote_flush_or_zap(kvm, &invalid_list, true); ++ } + + spte = make_nonleaf_spte(sp->spt, sp_ad_disabled(sp)); + +-- +2.43.0 + diff --git a/SOURCES/1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch b/SOURCES/1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch new file mode 100644 index 000000000..3119798ef --- /dev/null +++ b/SOURCES/1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch @@ -0,0 +1,67 @@ +From 81ccda30b4e83d8f5cc4fd50503c44e3a33abfeb Mon Sep 17 00:00:00 2001 +From: Paolo Bonzini +Date: Fri, 12 Jun 2026 22:18:12 +0200 +Subject: [PATCH] KVM: x86: Fix shadow paging use-after-free due to unexpected + role + +Commit 0cb2af2ea66ad ("KVM: x86: Fix shadow paging use-after-free due +to unexpected GFN") fixed a shadow paging mismatch between stored and +computed GFNs; the bug could be triggered by changing a PDE mapping from +outside the guest, and then deleting a memslot. The rmap_remove() +call would miss entries created after the PDE change because the GFN +of the leaf SPTE does not match the GFN of the struct kvm_mmu_page. + +A similar hole however remains if the modified PDE points to a non-leaf +page. In this case the gfn can be made to match, but the role does not +match: the original large 2MB page creates a kvm_mmu_page with direct=1, +while the new 4KB needs a kvm_mmu_page with direct=0. However, +kvm_mmu_get_child_sp() does not compare the role, and therefore reuses +the page. + +The next step is installing a leaf (4KB) SPTE on the new path which +records an rmap entry under the gfn resolved by the walk. But when +that child is zapped its parent kvm_mmu_page has direct=1 and +kvm_mmu_page_get_gfn() computes the gfn for the 4KB page as +sp->gfn + index instead of using sp->shadowed_translation[] (or sp->gfns[] +in older kernels). It therefore fails to remove the recorded entry. + +When the memslot is dropped the shadow page is freed but the rmap +entry survives, as in the scenario that was already fixed. Code that +later walks that gfn (dirty logging, MMU notifier invalidation, and +so on) dereferences an sptep that lies in the freed page, causing the +use-after-free. + +Fixes: 2032a93d66fa ("KVM: MMU: Don't allocate gfns page for direct mmu pages") +Reported-by: Hyunwoo Kim +Signed-off-by: Paolo Bonzini +--- + arch/x86/kvm/mmu/mmu.c | 10 ++++++---- + 1 file changed, 6 insertions(+), 4 deletions(-) + +diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c +index 6140702..8996c89 100644 +--- a/arch/x86/kvm/mmu/mmu.c ++++ b/arch/x86/kvm/mmu/mmu.c +@@ -2315,13 +2315,15 @@ static struct kvm_mmu_page *kvm_mmu_get_child_sp(struct kvm_vcpu *vcpu, + u64 *sptep, gfn_t gfn, + bool direct, unsigned int access) + { +- union kvm_mmu_page_role role; ++ union kvm_mmu_page_role role = kvm_mmu_child_role(sptep, direct, access); + +- if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep) && +- spte_to_child_sp(*sptep) && spte_to_child_sp(*sptep)->gfn == gfn) ++ if (is_shadow_present_pte(*sptep) && ++ !is_large_pte(*sptep) && ++ spte_to_child_sp(*sptep) && ++ spte_to_child_sp(*sptep)->gfn == gfn && ++ spte_to_child_sp(*sptep)->role.word == role.word) + return ERR_PTR(-EEXIST); + +- role = kvm_mmu_child_role(sptep, direct, access); + return kvm_mmu_get_shadow_page(vcpu, gfn, role); + } + +-- +2.43.0 + diff --git a/SPECS/kernel.spec b/SPECS/kernel.spec index 65df539d2..a211c1885 100644 --- a/SPECS/kernel.spec +++ b/SPECS/kernel.spec @@ -182,7 +182,7 @@ Summary: The Linux kernel # This is needed to do merge window version magic %define patchlevel 14 # This allows pkg_release to have configurable %%{?dist} tag -%define specrelease 687.20.1%{?buildid}%{?dist} +%define specrelease 687.20.2%{?buildid}%{?dist} # This defines the kabi tarball version %define kabiversion 5.14.0-687.5.1.el9_8 @@ -1587,6 +1587,8 @@ Patch1706: 1706-crypto-testmgr-allow-authenc-hmac-sha-256-384-cts-cbc-aes-in.pat Patch1707: 1707-crypto-krb5enc-fix-sleepable-flag-handling-in-encrypt-dispat.patch Patch1708: 1708-crypto-krb5enc-fix-async-decrypt-skipping-hash-verification.patch Patch1709: 1709-crypto-krb5-filter-out-async-aead-implementations-at-alloc.patch +Patch1710: 1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch +Patch1711: 1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch # END OF PATCH DEFINITIONS %description @@ -2941,6 +2943,8 @@ ApplyPatch 1706-crypto-testmgr-allow-authenc-hmac-sha-256-384-cts-cbc-aes-in.pat ApplyPatch 1707-crypto-krb5enc-fix-sleepable-flag-handling-in-encrypt-dispat.patch ApplyPatch 1708-crypto-krb5enc-fix-async-decrypt-skipping-hash-verification.patch ApplyPatch 1709-crypto-krb5-filter-out-async-aead-implementations-at-alloc.patch +ApplyPatch 1710-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-gfn.patch +ApplyPatch 1711-kvm-x86-fix-shadow-paging-use-after-free-due-to-unexpected-role.patch # END OF PATCH APPLICATIONS # Any further pre-build tree manipulations happen here. @@ -5015,6 +5019,10 @@ fi # # %changelog +* Thu Jul 02 2026 Andrew Lukoshko - 5.14.0-687.20.2 +- Fix KVM x86 shadow paging use-after-free due to unexpected GFN ahead of RHEL (1710) +- Fix KVM x86 shadow paging use-after-free due to unexpected role ahead of RHEL (1711) + * Tue Jun 30 2026 Andrew Lukoshko - 5.14.0-687.20.1 - Recreate RHEL 5.14.0-687.20.1 from CentOS Stream 9 and upstream stable backports (1696-1709) - Retain AlmaLinux ahead-of-RHEL fix for CVE-2026-46316 (1312)