Xen Security Advisory 486 v2 (CVE-2026-23558) - grant table v2 race in status page mapping

Xen.org security team posted 1 patch 5 days, 1 hour ago
Failed in applying to current master (apply log)
Xen Security Advisory 486 v2 (CVE-2026-23558) - grant table v2 race in status page mapping
Posted by Xen.org security team 5 days, 1 hour ago
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

            Xen Security Advisory CVE-2026-23558 / XSA-486
                               version 2

              grant table v2 race in status page mapping

UPDATES IN VERSION 2
====================

Public release.

ISSUE DESCRIPTION
=================

The adjustments made for XSA-379 as well as those subsequently becoming
XSA-387 still left a race window, when a HVM or PVH guest does a grant
table version change from v2 to v1 in parallel with mapping the status
page(s) via XENMEM_add_to_physmap.  Some of the status pages may then be
freed while mappings of them would still be inserted into the guest's
secondary (P2M) page tables.

IMPACT
======

Privilege escalation, information leaks, and Denial of Service (DoS) up
to affecting the entire host cannot be excluded.

VULNERABLE SYSTEMS
==================

All Xen versions from 4.0 onwards are affected.  Xen versions 3.4 and
older are not affected.

Only x86 HVM and PVH guests permitted to use grant table version 2
interfaces can leverage this vulnerability.  x86 PV guests cannot
leverage this vulnerability.  On Arm, grant table v2 use is explicitly
unsupported.

MITIGATION
==========

Using the "gnttab=max-ver:1" hypervisor command line option will avoid
the vulnerability.

Using the "max_grant_version=1" guest configuration option for HVM and PVH
guests will also avoid the vulnerability.

CREDITS
=======

This issue was discovered by Claude Opus 4.6 and diagnosed as a security
issue by Rafal Wojtczuk.

RESOLUTION
==========

Applying the appropriate attached patch resolves this issue.

Note that patches for released versions are generally prepared to
apply to the stable branches, and may not apply cleanly to the most
recent release tarball.  Downstreams are encouraged to update to the
tip of the stable branch before applying these patches.

xsa486.patch           xen-unstable - Xen 4.19.x
xsa486-4.18.patch      Xen 4.18.x - Xen 4.17.x

$ sha256sum xsa486*
0bc1336f0d8de463e30a920bb900b0199a79b4cc19af72e64cfb60504fa6599d  xsa486.patch
3fa23326a2761eba62e661fa052c1cd6b69041ea6752ed573ab240ebcdffedf8  xsa486-4.18.patch
$

DEPLOYMENT DURING EMBARGO
=========================

Deployment of the patches described above (or others which are
substantially similar) is permitted during the embargo, even on public-
facing systems with untrusted guest users and administrators.

HOWEVER, deployment of the mitigation is NOT permitted (except where
all the affected systems and VMs are administered and used only by
organisations which are members of the Xen Project Security Issues
Predisclosure List).  Specifically, deployment on public cloud systems
is NOT permitted.

This is because restricting the available grant table version is a guest
visible configuration change, which may lead to re-discovery of the issue.

Deployment of this mitigation is permitted only AFTER the embargo ends.

AND: Distribution of updated software is prohibited (except to other
members of the predisclosure list).

Predisclosure list members who wish to deploy significantly different
patches and/or mitigations, please contact the Xen Project Security
Team.

(Note: this during-embargo deployment notice is retained in
post-embargo publicly released Xen Project advisories, even though it
is then no longer applicable.  This is to enable the community to have
oversight of the Xen Project Security Team's decisionmaking.)

For more information about permissible uses of embargoed information,
consult the Xen Project community's agreed Security Policy:
  http://www.xenproject.org/security-policy.html
-----BEGIN PGP SIGNATURE-----

iQFABAEBCAAqFiEEI+MiLBRfRHX6gGCng/4UyVfoK9kFAmnwoQMMHHBncEB4ZW4u
b3JnAAoJEIP+FMlX6CvZKXgH/1/L4sRCjLuuwnugfhgcfYdOwFfWEsBGhxsuYTHT
61mqh8Ft4asiPf0qSUJzcWCpfKCB8aGBAEWDj7Hle+yAgYZ22Inf4j2emfcehXiu
hkKJ+2VgYs0C4xK1mOrPysxXha9pbyNvEHBJP794QitUYIzuJzeNAcKPmzR10rZ3
jEpyLC41sGiftIB/jq579Mrvz2cp02l2L77+zeWogl7ZMLPs+GbRoF1chTrIo9DU
Rt9WJnF7hD+elk280nwO2N6OCgrEVRmSR6AjsGb3E6JGUmZYJ6ZTtEaV+2TBiCXH
rfrJGwftJLp6a54RRDPjK709itzppJGPG/ur2rrIRxenRcY=
=1e9B
-----END PGP SIGNATURE-----
From: Jan Beulich <jbeulich@suse.com>
Subject: gnttab: split gnttab_map_frame()

If a domain tries to map status frames in parallel to switching grant
table version from 2 to 1, the mapping operation may put in place P2M
entries referencing MFNs which gnttab_unpopulate_status_frames() is in the
process of freeing.

Ideally we would refcount pages when entered into P2M tables, but that's a
significant change. Extend the grant-table-locked region instead in
xenmem_add_to_physmap_one() (being the sole caller of gnttab_map_frame()),
such that a race with gnttab_unpopulate_status_frames() is no longer
possible.

This is XSA-486 / CVE-2026-23558.

Fixes: 5ce8fafa947c ("Dynamic grant-table sizing")
Fixes: a98dc13703e0 ("Introduce a grant_entry_v2 structure")
Reported-by: Rafal Wojtczuk <rafal.wojtczuk@7bulls.com>
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Roger Pau Monné <roger.pau@citrix.com>

--- a/xen/arch/arm/mm.c
+++ b/xen/arch/arm/mm.c
@@ -174,12 +174,10 @@ int xenmem_add_to_physmap_one(
     switch ( space )
     {
     case XENMAPSPACE_grant_table:
-        rc = gnttab_map_frame(d, idx, gfn, &mfn);
+        rc = gnttab_map_frame_begin(d, idx, gfn, &mfn);
         if ( rc )
             return rc;
 
-        /* Need to take care of the reference obtained in gnttab_map_frame(). */
-        page = mfn_to_page(mfn);
         t = p2m_ram_rw;
 
         break;
@@ -281,10 +279,23 @@ int xenmem_add_to_physmap_one(
      * to drop the reference we took earlier. In all other cases we need to
      * drop any reference we took earlier (perhaps indirectly).
      */
-    if ( space == XENMAPSPACE_gmfn_foreign ? rc : page != NULL )
+    switch ( space )
     {
+    default:
+        if ( page )
+            put_page(page);
+        break;
+
+    case XENMAPSPACE_grant_table:
+        gnttab_map_frame_end(d, mfn);
+        break;
+
+    case XENMAPSPACE_gmfn_foreign:
+        if ( !rc )
+            break;
         ASSERT(page != NULL);
         put_page(page);
+        break;
     }
 
     return rc;
--- a/xen/arch/x86/mm/p2m.c
+++ b/xen/arch/x86/mm/p2m.c
@@ -2009,11 +2009,9 @@ int xenmem_add_to_physmap_one(
         break;
 
     case XENMAPSPACE_grant_table:
-        rc = gnttab_map_frame(d, idx, gfn, &mfn);
+        rc = gnttab_map_frame_begin(d, idx, gfn, &mfn);
         if ( rc )
             return rc;
-        /* Need to take care of the reference obtained in gnttab_map_frame(). */
-        page = mfn_to_page(mfn);
         break;
 
     case XENMAPSPACE_gmfn:
@@ -2095,19 +2093,28 @@ int xenmem_add_to_physmap_one(
     put_gfn(d, gfn_x(gfn));
 
  put_both:
-    /*
-     * In the XENMAPSPACE_gmfn case, we took a ref of the gfn at the top.
-     * We also may need to transfer ownership of the page reference to our
-     * caller.
-     */
-    if ( space == XENMAPSPACE_gmfn )
+    switch ( space )
     {
+    case XENMAPSPACE_gmfn:
+        /*
+         * We took a ref of the gfn at the top.  We also may need to transfer
+         * ownership of the page reference to our caller.
+         */
         put_gfn(d, gmfn);
         if ( !rc && extra.ppage )
         {
             *extra.ppage = page;
             page = NULL;
         }
+        break;
+
+    case XENMAPSPACE_grant_table:
+        /*
+         * We (gnttab_map_frame_begin()) acquired a lock and took a ref of the
+         * page underlying the MFN at the top.
+         */
+        gnttab_map_frame_end(d, mfn);
+        break;
     }
 
     if ( page )
--- a/xen/common/grant_table.c
+++ b/xen/common/grant_table.c
@@ -4250,7 +4250,8 @@ int gnttab_acquire_resource(
     return rc;
 }
 
-int gnttab_map_frame(struct domain *d, unsigned long idx, gfn_t gfn, mfn_t *mfn)
+int gnttab_map_frame_begin(
+    struct domain *d, unsigned long idx, gfn_t gfn, mfn_t *mfn)
 {
     int rc = 0;
     struct grant_table *gt = d->grant_table;
@@ -4288,11 +4289,19 @@ int gnttab_map_frame(struct domain *d, u
             put_page(pg);
     }
 
-    grant_write_unlock(gt);
+    if ( rc )
+        grant_write_unlock(d->grant_table);
 
     return rc;
 }
 
+void gnttab_map_frame_end(struct domain *d, mfn_t mfn)
+{
+    put_page(mfn_to_page(mfn));
+
+    grant_write_unlock(d->grant_table);
+}
+
 static void gnttab_usage_print(struct domain *rd)
 {
     int first = 1;
--- a/xen/include/xen/grant_table.h
+++ b/xen/include/xen/grant_table.h
@@ -60,8 +60,13 @@ int gnttab_release_mappings(struct domai
 int mem_sharing_gref_to_gfn(struct grant_table *gt, grant_ref_t ref,
                             gfn_t *gfn, uint16_t *status);
 
-int gnttab_map_frame(struct domain *d, unsigned long idx, gfn_t gfn,
-                     mfn_t *mfn);
+/*
+ * These need to be used as a pair, as the first (in the success case) returns
+ * with a lock and page reference held which the second needs to drop.
+ */
+int gnttab_map_frame_begin(struct domain *d, unsigned long idx, gfn_t gfn,
+                           mfn_t *mfn);
+void gnttab_map_frame_end(struct domain *d, mfn_t mfn);
 
 unsigned int gnttab_resource_max_frames(const struct domain *d, unsigned int id);
 
@@ -100,12 +105,14 @@ static inline int mem_sharing_gref_to_gf
     return -EINVAL;
 }
 
-static inline int gnttab_map_frame(struct domain *d, unsigned long idx,
-                                   gfn_t gfn, mfn_t *mfn)
+static inline int gnttab_map_frame_begin(struct domain *d, unsigned long idx,
+                                         gfn_t gfn, mfn_t *mfn)
 {
     return -EINVAL;
 }
 
+static inline void gnttab_map_frame_end(struct domain *d, mfn_t mfn) {}
+
 static inline unsigned int gnttab_resource_max_frames(
     const struct domain *d, unsigned int id)
 {
From: Jan Beulich <jbeulich@suse.com>
Subject: gnttab: split gnttab_map_frame()

If a domain tries to map status frames in parallel to switching grant
table version from 2 to 1, the mapping operation may put in place P2M
entries referencing MFNs which gnttab_unpopulate_status_frames() is in the
process of freeing.

Ideally we would refcount pages when entered into P2M tables, but that's a
significant change. Extend the grant-table-locked region instead in
xenmem_add_to_physmap_one() (being the sole caller of gnttab_map_frame()),
such that a race with gnttab_unpopulate_status_frames() is no longer
possible.

This is XSA-486 / CVE-2026-23558.

Fixes: 5ce8fafa947c ("Dynamic grant-table sizing")
Fixes: a98dc13703e0 ("Introduce a grant_entry_v2 structure")
Reported-by: Rafal Wojtczuk <rafal.wojtczuk@7bulls.com>
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Roger Pau Monné <roger.pau@citrix.com>

--- a/xen/arch/arm/mm.c
+++ b/xen/arch/arm/mm.c
@@ -1372,12 +1372,10 @@ int xenmem_add_to_physmap_one(
     switch ( space )
     {
     case XENMAPSPACE_grant_table:
-        rc = gnttab_map_frame(d, idx, gfn, &mfn);
+        rc = gnttab_map_frame_begin(d, idx, gfn, &mfn);
         if ( rc )
             return rc;
 
-        /* Need to take care of the reference obtained in gnttab_map_frame(). */
-        page = mfn_to_page(mfn);
         t = p2m_ram_rw;
 
         break;
@@ -1479,10 +1477,23 @@ int xenmem_add_to_physmap_one(
      * to drop the reference we took earlier. In all other cases we need to
      * drop any reference we took earlier (perhaps indirectly).
      */
-    if ( space == XENMAPSPACE_gmfn_foreign ? rc : page != NULL )
+    switch ( space )
     {
+    default:
+        if ( page )
+            put_page(page);
+        break;
+
+    case XENMAPSPACE_grant_table:
+        gnttab_map_frame_end(d, mfn);
+        break;
+
+    case XENMAPSPACE_gmfn_foreign:
+        if ( !rc )
+            break;
         ASSERT(page != NULL);
         put_page(page);
+        break;
     }
 
     return rc;
--- a/xen/arch/x86/mm/p2m.c
+++ b/xen/arch/x86/mm/p2m.c
@@ -2446,11 +2446,9 @@ int xenmem_add_to_physmap_one(
         break;
 
     case XENMAPSPACE_grant_table:
-        rc = gnttab_map_frame(d, idx, gpfn, &mfn);
+        rc = gnttab_map_frame_begin(d, idx, gpfn, &mfn);
         if ( rc )
             return rc;
-        /* Need to take care of the reference obtained in gnttab_map_frame(). */
-        page = mfn_to_page(mfn);
         break;
 
     case XENMAPSPACE_gmfn:
@@ -2526,19 +2524,28 @@ int xenmem_add_to_physmap_one(
     put_gfn(d, gfn_x(gpfn));
 
  put_both:
-    /*
-     * In the XENMAPSPACE_gmfn case, we took a ref of the gfn at the top.
-     * We also may need to transfer ownership of the page reference to our
-     * caller.
-     */
-    if ( space == XENMAPSPACE_gmfn )
+    switch ( space )
     {
+    case XENMAPSPACE_gmfn:
+        /*
+         * We took a ref of the gfn at the top.  We also may need to transfer
+         * ownership of the page reference to our caller.
+         */
         put_gfn(d, gfn);
         if ( !rc && extra.ppage )
         {
             *extra.ppage = page;
             page = NULL;
         }
+        break;
+
+    case XENMAPSPACE_grant_table:
+        /*
+         * We (gnttab_map_frame_begin()) acquired a lock and took a ref of the
+         * page underlying the MFN at the top.
+         */
+        gnttab_map_frame_end(d, mfn);
+        break;
     }
 
     if ( page )
--- a/xen/common/grant_table.c
+++ b/xen/common/grant_table.c
@@ -4237,7 +4237,8 @@ int gnttab_acquire_resource(
     return rc;
 }
 
-int gnttab_map_frame(struct domain *d, unsigned long idx, gfn_t gfn, mfn_t *mfn)
+int gnttab_map_frame_begin(
+    struct domain *d, unsigned long idx, gfn_t gfn, mfn_t *mfn)
 {
     int rc = 0;
     struct grant_table *gt = d->grant_table;
@@ -4275,11 +4276,19 @@ int gnttab_map_frame(struct domain *d, u
             put_page(pg);
     }
 
-    grant_write_unlock(gt);
+    if ( rc )
+        grant_write_unlock(d->grant_table);
 
     return rc;
 }
 
+void gnttab_map_frame_end(struct domain *d, mfn_t mfn)
+{
+    put_page(mfn_to_page(mfn));
+
+    grant_write_unlock(d->grant_table);
+}
+
 static void gnttab_usage_print(struct domain *rd)
 {
     int first = 1;
--- a/xen/include/xen/grant_table.h
+++ b/xen/include/xen/grant_table.h
@@ -53,8 +53,13 @@ int gnttab_release_mappings(struct domai
 int mem_sharing_gref_to_gfn(struct grant_table *gt, grant_ref_t ref,
                             gfn_t *gfn, uint16_t *status);
 
-int gnttab_map_frame(struct domain *d, unsigned long idx, gfn_t gfn,
-                     mfn_t *mfn);
+/*
+ * These need to be used as a pair, as the first (in the success case) returns
+ * with a lock and page reference held which the second needs to drop.
+ */
+int gnttab_map_frame_begin(struct domain *d, unsigned long idx, gfn_t gfn,
+                           mfn_t *mfn);
+void gnttab_map_frame_end(struct domain *d, mfn_t mfn);
 
 unsigned int gnttab_resource_max_frames(const struct domain *d, unsigned int id);
 
@@ -93,12 +98,14 @@ static inline int mem_sharing_gref_to_gf
     return -EINVAL;
 }
 
-static inline int gnttab_map_frame(struct domain *d, unsigned long idx,
-                                   gfn_t gfn, mfn_t *mfn)
+static inline int gnttab_map_frame_begin(struct domain *d, unsigned long idx,
+                                         gfn_t gfn, mfn_t *mfn)
 {
     return -EINVAL;
 }
 
+static inline void gnttab_map_frame_end(struct domain *d, mfn_t mfn) {}
+
 static inline unsigned int gnttab_resource_max_frames(
     const struct domain *d, unsigned int id)
 {