Add fix for CVE-2026-46113 (KVM x86 shadow paging UAF) ahead of RHEL; bump to 553.139.3

This commit is contained in:
Andrew Lukoshko 2026-07-02 12:32:21 +00:00
parent 932faf1f47
commit 9787fb255b
2 changed files with 276 additions and 6 deletions

View File

@ -0,0 +1,253 @@
kpatch-cve: CVE-2026-46113
kpatch-cve-url: https://access.redhat.com/security/cve/CVE-2026-46113
kpatch-cvss: 8.8
kpatch-description: KVM: x86: Fix shadow paging use-after-free due to unexpected GFN/role
kpatch-kernel: 4.18.0-553.139.1.el8_10
kpatch-patch-url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=0cb2af2ea66ad8ff195c156ea690f11216285bdf
From: KernelCare Security Team
Subject: [PATCH] KVM: x86: Fix shadow paging use-after-free due to unexpected GFN/role
CVE: CVE-2026-46113
Upstream Status: manually adapted, not a literal cherry-pick. This
combines the effect of two upstream fixes:
- commit 0cb2af2ea66a ("KVM: x86: Fix shadow paging use-after-free
due to unexpected GFN"), which is the fix CVE-2026-46113 refers to
- commit 81ccda30b4e8 ("KVM: x86: Fix shadow paging use-after-free
due to unexpected role"), a follow-up closing a second, related
hole in the same code path (found while hardening it after the
first fix)
Neither commit applies to this kernel: both are written against
upstream's post-refactor MMU (kvm_mmu_get_child_sp()/
kvm_mmu_get_shadow_page()/__link_shadow_page(), introduced by a large
prerequisite series -- e.g. "KVM: x86/mmu: Derive shadow MMU page role
from parent" and "KVM: x86/mmu: pull call to drop_large_spte() into
__link_shadow_page()" -- that this 4.18-based kernel does not carry).
This kernel still uses the older, pre-refactor shape: each of the
three "walk into an existing non-leaf SPTE or install a new one" call
sites (kvm_mmu_get_page()+link_shadow_page() in __direct_map() and in
both loops of FNAME(fetch)) independently does:
drop_large_spte(vcpu, it.sptep);
if (!is_shadow_present_pte(*it.sptep)) {
sp = kvm_mmu_get_page(...);
link_shadow_page(vcpu, it.sptep, sp);
}
i.e. if *it.sptep is already present (and not a large leaf,
already handled by drop_large_spte()), the walk just continues into
whatever child it already points to, without checking that the child
actually matches the gfn/role being walked to. This is exactly the
root cause both upstream commits describe: if the guest's page tables
are modified between VM entries (shadow paging) such that a PDE now
resolves to a different gfn, or to a translation requiring a
differently-shaped shadow page (e.g. a 2MB direct-mapped leaf split
into a 4KB indirect page table), the stale child is reused as-is.
When that stale child is later zapped, kvm_mmu_page_get_gfn() derives
the wrong gfn for its rmap entries (sp->gfn + index, or sp->gfn
itself, instead of the actual mapped gfn), so rmap_remove() cannot
find and remove them. If the memslot backing the old translation is
then dropped, the shadow page is freed while the stale rmap entry
survives; any later rmap walk over that gfn (dirty logging, MMU
notifier invalidation such as MADV_DONTNEED, ...) dereferences freed
memory.
Fix this at each of the three call sites by validating, before
falling through to "reuse the existing child", that the present
non-large SPTE actually points to a child with the expected gfn *and*
role (mirroring 81ccda30b4e8's role check on top of 0cb2af2ea66a's gfn
check). If it doesn't, the stale SPTE (and its subtree) is torn down
via mmu_page_zap_pte() + kvm_mmu_remote_flush_or_zap() -- the same
primitives upstream's __link_shadow_page() now uses -- before
installing the correct child. This preserves the existing fast path
(no hash-table lookup) for the common case where the already-linked
child is correct.
The role/gfn comparison itself is factored into two small helpers,
kvm_mmu_child_role() (the role computation already done at the top of
kvm_mmu_get_page(), extracted unchanged so both functions share it)
and shadow_page_role_matches().
Note on kernel commit a955cad84cda ("KVM: x86/mmu: Retry page fault if
root is invalidated by memslot update"): during upstream's stable
backport of 0cb2af2ea66a to 5.10.y/5.15.y, this turned out to be a
required prerequisite -- without it, testers hit WARN_ON regressions
in kvm_mmu_zap_all_fast()/kvm_mmu_zap_all() (see the "stable backports
for ..." thread on kvm@vger.kernel.org, message
20260630223723.83727-1-zcgao@amazon.com). This kernel already has the
equivalent logic (is_page_fault_stale()/is_obsolete_sp(), used in both
direct_page_fault() and FNAME(page_fault)), confirmed present before
writing this patch, so no separate prerequisite patch is needed here.
Reported-by: Hyunwoo Kim <imv4bel@gmail.com>
Reported-by: Alexander Bulekov <bkov@amazon.com>
Reported-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
arch/x86/kvm/mmu/mmu.c | 72 ++++++++++++++++++++++++++++++++++-------
arch/x86/kvm/mmu/paging_tmpl.h | 40 ++++++++++++++++++++--
2 files changed, 96 insertions(+), 16 deletions(-)
diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c
index c4854dc02..696c3b4c6 100644
--- a/arch/x86/kvm/mmu/mmu.c
+++ b/arch/x86/kvm/mmu/mmu.c
@@ -2002,21 +2002,13 @@ static void clear_sp_write_flooding_count(u64 *spte)
__clear_sp_write_flooding_count(sptep_to_sp(spte));
}
-static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu,
- gfn_t gfn,
- gva_t gaddr,
- unsigned level,
- int direct,
- unsigned int access)
+static union kvm_mmu_page_role kvm_mmu_child_role(struct kvm_vcpu *vcpu,
+ gva_t gaddr, unsigned level,
+ int direct,
+ unsigned int access)
{
- bool direct_mmu = vcpu->arch.mmu->direct_map;
union kvm_mmu_page_role role;
- struct hlist_head *sp_list;
unsigned quadrant;
- struct kvm_mmu_page *sp;
- int ret;
- int collisions = 0;
- LIST_HEAD(invalid_list);
role = vcpu->arch.mmu->mmu_role.base;
role.level = level;
@@ -2028,6 +2020,46 @@ static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu,
role.quadrant = quadrant;
}
+ return role;
+}
+
+/*
+ * Returns true if @sptep already links to a present, non-large shadow page
+ * that matches @gfn and @role, i.e. it is safe to keep walking through the
+ * existing child instead of replacing it. The gfn of a direct shadow page
+ * only tracks its *first* mapping, so an intervening guest PTE change can
+ * leave a present SPTE pointing at a child that was allocated for a
+ * different gfn and/or role; blindly reusing it leads to a stale rmap entry
+ * (and eventually a use-after-free) once the mismatched child is zapped.
+ */
+static bool shadow_page_role_matches(u64 *sptep, gfn_t gfn,
+ union kvm_mmu_page_role role)
+{
+ struct kvm_mmu_page *child;
+
+ if (!is_shadow_present_pte(*sptep) || is_large_pte(*sptep))
+ return false;
+
+ child = to_shadow_page(*sptep & PT64_BASE_ADDR_MASK);
+ return child->gfn == gfn && child->role.word == role.word;
+}
+
+static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu,
+ gfn_t gfn,
+ gva_t gaddr,
+ unsigned level,
+ int direct,
+ unsigned int access)
+{
+ bool direct_mmu = vcpu->arch.mmu->direct_map;
+ union kvm_mmu_page_role role = kvm_mmu_child_role(vcpu, gaddr, level,
+ direct, access);
+ struct hlist_head *sp_list;
+ struct kvm_mmu_page *sp;
+ int ret;
+ int collisions = 0;
+ LIST_HEAD(invalid_list);
+
sp_list = &vcpu->kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)];
for_each_valid_sp(vcpu->kvm, sp, sp_list) {
if (sp->gfn != gfn) {
@@ -2946,8 +2978,20 @@ static int __direct_map(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault)
break;
drop_large_spte(vcpu, it.sptep);
- if (is_shadow_present_pte(*it.sptep))
- continue;
+ if (is_shadow_present_pte(*it.sptep)) {
+ union kvm_mmu_page_role role;
+ LIST_HEAD(invalid_list);
+
+ role = kvm_mmu_child_role(vcpu, it.addr, it.level - 1,
+ true, ACC_ALL);
+ if (shadow_page_role_matches(it.sptep, base_gfn, role))
+ continue;
+
+ mmu_page_zap_pte(vcpu->kvm, sptep_to_sp(it.sptep),
+ it.sptep, &invalid_list);
+ kvm_mmu_remote_flush_or_zap(vcpu->kvm, &invalid_list,
+ true);
+ }
sp = kvm_mmu_get_page(vcpu, base_gfn, it.addr,
it.level - 1, true, ACC_ALL);
diff --git a/arch/x86/kvm/mmu/paging_tmpl.h b/arch/x86/kvm/mmu/paging_tmpl.h
index 3b825e60a..911dbe698 100644
--- a/arch/x86/kvm/mmu/paging_tmpl.h
+++ b/arch/x86/kvm/mmu/paging_tmpl.h
@@ -676,10 +676,28 @@ static int FNAME(fetch)(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault,
clear_sp_write_flooding_count(it.sptep);
drop_large_spte(vcpu, it.sptep);
+ table_gfn = gw->table_gfn[it.level - 2];
+ access = gw->pt_access[it.level - 2];
+
+ if (is_shadow_present_pte(*it.sptep)) {
+ union kvm_mmu_page_role role;
+ LIST_HEAD(invalid_list);
+
+ role = kvm_mmu_child_role(vcpu, fault->addr,
+ it.level - 1, false, access);
+ if (!shadow_page_role_matches(it.sptep, table_gfn,
+ role)) {
+ mmu_page_zap_pte(vcpu->kvm,
+ sptep_to_sp(it.sptep),
+ it.sptep, &invalid_list);
+ kvm_mmu_remote_flush_or_zap(vcpu->kvm,
+ &invalid_list,
+ true);
+ }
+ }
+
sp = NULL;
if (!is_shadow_present_pte(*it.sptep)) {
- table_gfn = gw->table_gfn[it.level - 2];
- access = gw->pt_access[it.level - 2];
sp = kvm_mmu_get_page(vcpu, table_gfn, fault->addr,
it.level-1, false, access);
/*
@@ -736,6 +754,24 @@ static int FNAME(fetch)(struct kvm_vcpu *vcpu, struct kvm_page_fault *fault,
drop_large_spte(vcpu, it.sptep);
+ if (is_shadow_present_pte(*it.sptep)) {
+ union kvm_mmu_page_role role;
+ LIST_HEAD(invalid_list);
+
+ role = kvm_mmu_child_role(vcpu, fault->addr,
+ it.level - 1, true,
+ direct_access);
+ if (!shadow_page_role_matches(it.sptep, base_gfn,
+ role)) {
+ mmu_page_zap_pte(vcpu->kvm,
+ sptep_to_sp(it.sptep),
+ it.sptep, &invalid_list);
+ kvm_mmu_remote_flush_or_zap(vcpu->kvm,
+ &invalid_list,
+ true);
+ }
+ }
+
if (!is_shadow_present_pte(*it.sptep)) {
sp = kvm_mmu_get_page(vcpu, base_gfn, fault->addr,
it.level - 1, true, direct_access);
--
2.39.2

View File

@ -49,10 +49,11 @@
# define buildid .local
%define specversion 4.18.0
%define pkgrelease 553.139.1.el8_10
%define pkgrelease 553.139.3.el8_10
%define tarfile_release 553.139.1.el8_10
# allow pkg_release to have configurable %%{?dist} tag
%define specrelease 553.139.1%{?dist}
%define specrelease 553.139.3%{?dist}
%define pkg_release %{specrelease}%{?buildid}
@ -446,7 +447,7 @@ BuildRequires: xmlto
BuildRequires: asciidoc
%endif
Source0: linux-%{specversion}-%{pkgrelease}.tar.xz
Source0: linux-%{specversion}-%{tarfile_release}.tar.xz
Source9: x509.genkey
@ -551,6 +552,9 @@ Patch2006: 0006-Bring-back-deprecated-pci-ids-to-lpfc-driver.patch
Patch2007: 0007-Bring-back-deprecated-pci-ids-to-qla4xxx-driver.patch
Patch2008: 0008-Bring-back-deprecated-pci-ids-to-be2iscsi-driver.patch
# AlmaLinux ahead-of-RHEL security fixes
Patch1100: 1100-kvm-x86-fix-shadow-paging-use-after-free.patch
# END OF PATCH DEFINITIONS
BuildRoot: %{_tmppath}/%{name}-%{KVERREL}-root
@ -1108,9 +1112,9 @@ ApplyOptionalPatch()
fi
}
%setup -q -n %{name}-%{specversion}-%{pkgrelease} -c
cp -v %{SOURCE9000} linux-%{specversion}-%{pkgrelease}/certs/rhel.pem
mv linux-%{specversion}-%{pkgrelease} linux-%{KVERREL}
%setup -q -n %{name}-%{specversion}-%{tarfile_release} -c
cp -v %{SOURCE9000} linux-%{specversion}-%{tarfile_release}/certs/rhel.pem
mv linux-%{specversion}-%{tarfile_release} linux-%{KVERREL}
cd linux-%{KVERREL}
@ -1128,6 +1132,9 @@ ApplyPatch 0006-Bring-back-deprecated-pci-ids-to-lpfc-driver.patch
ApplyPatch 0007-Bring-back-deprecated-pci-ids-to-qla4xxx-driver.patch
ApplyPatch 0008-Bring-back-deprecated-pci-ids-to-be2iscsi-driver.patch
# AlmaLinux ahead-of-RHEL security fixes
ApplyPatch 1100-kvm-x86-fix-shadow-paging-use-after-free.patch
# END OF PATCH APPLICATIONS
# Any further pre-build tree manipulations happen here.
@ -2729,6 +2736,16 @@ fi
#
#
%changelog
* Thu Jul 02 2026 Andrei Lukoshko <alukoshko@almalinux.org> - 4.18.0-553.139.3
- Replace CVE-2026-46113 backport patch series with a single combined
adaptation of upstream 0cb2af2ea66a and 81ccda30b4e8 (1100)
* Thu Jul 02 2026 Andrei Lukoshko <alukoshko@almalinux.org> - 4.18.0-553.139.2
- Fix CVE-2026-46113: KVM x86 shadow paging use-after-free due to unexpected
GFN, backported ahead of RHEL from the 5.15.y stable series with its
prerequisites and the follow-up unexpected-role and hugepage-recovery
fixes (1100-1107)
* Tue Jun 30 2026 Andrei Lukoshko <alukoshko@almalinux.org> - 4.18.0-553.139.1
- hpsa: bring back deprecated PCI ids #CFHack #CFHack2024
- mptsas: bring back deprecated PCI ids #CFHack #CFHack2024