From: Jan Beulich <jbeulich@suse.com>
Subject: vNUMA: close race window in domain cleanup

Calling vnuma_destroy() without any locking can race the handling of both
XENMEM_get_vnumainfo and XEN_DOMCTL_setvnumainfo. While the latter is,
without Flask, strictly only exposed to the control domain, the former
can also be invoked by a stubdom DM or a de-privileged DM running in the
control domain. Isolate the logic used by XEN_DOMCTL_setvnumainfo into a
new helper function, which then is also used from domain_kill(). While
doing so also move the vnuma_destroy() invocation out of the locked
region.

With d->vnuma properly cleared by domain_kill(), XENMEM_get_vnumainfo now
really only needs to check for the field being NULL. That check needs
repeating, though, after re-acquiring the lock.

This is CVE-2026-62429 / XSA-502.

Fixes: 9695014966b5 ("xen: vnuma topology and subop hypercalls")
Reported-by: Teddy Astie <teddy.astie@vates.tech>
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Juergen Gross <jgross@suse.com>

--- a/xen/common/domain.c
+++ b/xen/common/domain.c
@@ -1320,7 +1320,7 @@ int domain_kill(struct domain *d)
         d->is_dying = DOMDYING_dying;
         rspin_barrier(&d->domain_lock);
         argo_destroy(d);
-        vnuma_destroy(d->vnuma);
+        vnuma_replace(d, NULL);
         domain_set_outstanding_pages(d, 0);
         /* fallthrough */
     case DOMDYING_dying:
--- a/xen/common/domctl.c
+++ b/xen/common/domctl.c
@@ -140,7 +140,7 @@ void domctl_lock_release(void)
     spin_unlock(&current->domain->hypercall_deadlock_mutex);
 }
 
-void vnuma_destroy(struct vnuma_info *vnuma)
+static void vnuma_destroy(struct vnuma_info *vnuma)
 {
     if ( vnuma )
     {
@@ -152,6 +152,19 @@ void vnuma_destroy(struct vnuma_info *vn
     }
 }
 
+/* Overwrite (replace) vnuma topology for a domain. */
+void vnuma_replace(struct domain *d, struct vnuma_info *vnuma)
+{
+    struct vnuma_info *old;
+
+    write_lock(&d->vnuma_rwlock);
+    old = d->vnuma;
+    d->vnuma = vnuma;
+    write_unlock(&d->vnuma_rwlock);
+
+    vnuma_destroy(old);
+}
+
 /*
  * Allocates memory for vNUMA, **vnuma should be NULL.
  * Caller has to make sure that domain has max_pages
@@ -895,12 +908,7 @@ long do_domctl(XEN_GUEST_HANDLE_PARAM(xe
             break;
         }
 
-        /* overwrite vnuma topology for domain. */
-        write_lock(&d->vnuma_rwlock);
-        vnuma_destroy(d->vnuma);
-        d->vnuma = vnuma;
-        write_unlock(&d->vnuma_rwlock);
-
+        vnuma_replace(d, vnuma);
         break;
     }
 
--- a/xen/common/memory.c
+++ b/xen/common/memory.c
@@ -1922,12 +1922,24 @@ long do_memory_op(unsigned long cmd, XEN
             goto vnumainfo_out;
         }
 
+        read_lock(&d->vnuma_rwlock);
+
+        /*
+         * Check d->vnuma again after re-acquiring the lock as we can race
+         * with domain destruction.
+         */
+        if ( !d->vnuma )
+        {
+            ASSERT(d->is_dying);
+            read_unlock(&d->vnuma_rwlock);
+            rc = -ESRCH;
+            goto vnumainfo_out;
+        }
+
         /*
          * Check if vnuma info has changed and if the allocated arrays
          * are not big enough.
          */
-        read_lock(&d->vnuma_rwlock);
-
         if ( dom_vnodes < d->vnuma->nr_vnodes ||
              dom_vranges < d->vnuma->nr_vmemranges ||
              dom_vcpus < d->max_vcpus )
--- a/xen/include/xen/domain.h
+++ b/xen/include/xen/domain.h
@@ -193,9 +193,10 @@ struct vnuma_info {
 };
 
 #ifndef CONFIG_PV_SHIM_EXCLUSIVE
-void vnuma_destroy(struct vnuma_info *vnuma);
+void vnuma_replace(struct domain *d, struct vnuma_info *vnuma);
 #else
-static inline void vnuma_destroy(struct vnuma_info *vnuma) { ASSERT(!vnuma); }
+static inline void vnuma_replace(struct domain *d, struct vnuma_info *vnuma)
+{ ASSERT(!vnuma); }
 #endif
 
 #ifdef CONFIG_VMTRACE
