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 @@ -1003,7 +1003,7 @@ int domain_kill(struct domain *d) d->is_dying = DOMDYING_dying; spin_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 @@ -153,7 +153,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 ) { @@ -165,6 +165,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 @@ -908,12 +921,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 @@ -1761,12 +1761,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 @@ -159,9 +159,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;