From: Jan Beulich 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 Signed-off-by: Jan Beulich Reviewed-by: Juergen Gross --- a/xen/common/domain.c +++ b/xen/common/domain.c @@ -1255,7 +1255,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 @@ -142,7 +142,7 @@ void domctl_lock_release(void) spin_unlock(¤t->domain->hypercall_deadlock_mutex); } -void vnuma_destroy(struct vnuma_info *vnuma) +static void vnuma_destroy(struct vnuma_info *vnuma) { if ( vnuma ) { @@ -154,6 +154,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 @@ -907,12 +920,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 @@ -1804,12 +1804,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 @@ -188,9 +188,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 extern bool vmtrace_available;