-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
Xen Security Advisory CVE-2026-42487 / XSA-491
version 2
x86 HVM I/O port list traversal
UPDATES IN VERSION 2
====================
Public release.
ISSUE DESCRIPTION
=================
HVM guest I/O port accesses are subject to either emulation or at least
translation. Translations are managed by the device model (via
XEN_DOMCTL_ioport_mapping), and hence the linked list used may changed
at any time. Traversal of those lists (while handling guest I/O port
accesses) therefore needs synchronizing with updates, which was missing
so far.
IMPACT
======
A device model of a HVM guest can cause a hypervisor crash, causing a
Denial of Service (DoS) of the entire host. Privilege escalation and
information leaks cannot be ruled out.
VULNERABLE SYSTEMS
==================
All Xen versions from at least 3.2 onwards are vulnerable. Earlier
versions have not been inspected.
Only x86 systems are vulnerable. Arm systems are not vulnerable.
Only entities controlling HVM guests can leverage the vulnerability.
These are device models running in either a stub domain or de-privileged
in Dom0.
MITIGATION
==========
Running only PV or PVH guests will avoid the vulnerability.
(Switching from a device model stub domain or a de-privileged device
model to a fully privileged Dom0 device model does NOT mitigate this
vulnerability. Rather, it simply recategorises the vulnerability to
hostile management code, regarding it "as designed"; thus it merely
reclassifies these issues as "not a bug". The security of a Xen system
using stub domains is still better than with a qemu-dm running as a Dom0
process. Users and vendors of stub qemu dm systems should not change
their configuration to use a Dom0 qemu process.)
CREDITS
=======
This issue was discovered by Jan Beulich of SUSE.
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.
xsa491.patch xen-unstable
xsa491-4.21.patch Xen 4.21.x - Xen 4.17.x
$ sha256sum xsa491*
23a90da1c71389083351846169fc565a671b44f5f4ba838b18fc0fa6d7582bf8 xsa491.patch
443674f42a092b953b6ba4d91cfa19bfbee0077dfcd5a39ae53368e40ed23aac xsa491-4.21.patch
$
DEPLOYMENT DURING EMBARGO
=========================
Deployment of the patches and/or mitigations described above (or
others which are substantially similar) is permitted during the
embargo, even on public-facing systems with untrusted guest users and
administrators.
But: 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/4UyVfoK9kFAmon+4gMHHBncEB4ZW4u
b3JnAAoJEIP+FMlX6CvZGqMIAJ3p3v2yhpMhVPL7ClsuYuz8ks1cVHn4d4971wCS
gao1KbD+k8xjiqrR5pdCO/cHViXmajPk7sV4NwOsSmW1KQ8ejQrps3v16/IOTIjp
JzcDRqk2J6IurQE819kIe0B7vQlgfElK1ZUq070DljChzBwcuWnaXywacgh/eofo
SpElIHLtlM9RPmPTPaAI5inEIANb2Rrqdgt6yUg3XqSUN77h4ma8GLZH+Tt2x6Zg
HN9BjZcSmcRkOwWK80g30rQ0ZltSSh0ExM5Jhk0vtulbK5BeO7dAphElwbBjAwb2
RjuoQhvS4QkvCEZGpUIiFJKtxlixhqZZl9CFYm0b4Xe/aJA=
=8fKa
-----END PGP SIGNATURE-----
From: Jan Beulich <jbeulich@suse.com>
Subject: x86/HVM: add locking to I/O port translation list traversal
XEN_DOMCTL_ioport_mapping is usable by DM stubdoms, and hence we can't
assume the list to be left unaltered while the guest (really: the
hypervisor on behalf of the guest) is accessing it.
This is XSA-491 / CVE-2026-42487.
Fixes: 192c4dabc344 ("domctl and p2m changes for PCI passthru")
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Roger Pau Monné <roger.pau@citrix.com>
--- a/xen/arch/x86/domctl.c
+++ b/xen/arch/x86/domctl.c
@@ -667,6 +667,7 @@ long arch_do_domctl(
"ioport_map:add: dom%d gport=%x mport=%x nr=%x\n",
d->domain_id, fgp, fmp, np);
+ write_lock(&hvm->g2m_ioport_lock);
list_for_each_entry(g2m_ioport, &hvm->g2m_ioport_list, list)
if (g2m_ioport->mport == fmp )
{
@@ -688,11 +689,14 @@ long arch_do_domctl(
g2m_ioport->np = np;
list_add_tail(&g2m_ioport->list, &hvm->g2m_ioport_list);
}
+ write_unlock(&hvm->g2m_ioport_lock);
if ( !ret )
ret = ioports_permit_access(d, fmp, fmp + np - 1);
if ( ret && !found && g2m_ioport )
{
+ write_lock(&hvm->g2m_ioport_lock);
list_del(&g2m_ioport->list);
+ write_unlock(&hvm->g2m_ioport_lock);
xfree(g2m_ioport);
}
}
@@ -701,6 +705,8 @@ long arch_do_domctl(
printk(XENLOG_G_INFO
"ioport_map:remove: dom%d gport=%x mport=%x nr=%x\n",
d->domain_id, fgp, fmp, np);
+
+ write_lock(&hvm->g2m_ioport_lock);
list_for_each_entry(g2m_ioport, &hvm->g2m_ioport_list, list)
if ( g2m_ioport->mport == fmp )
{
@@ -708,6 +714,8 @@ long arch_do_domctl(
xfree(g2m_ioport);
break;
}
+ write_unlock(&hvm->g2m_ioport_lock);
+
ret = ioports_deny_access(d, fmp, fmp + np - 1);
if ( ret && is_hardware_domain(currd) )
printk(XENLOG_ERR
--- a/xen/arch/x86/hvm/emulate.c
+++ b/xen/arch/x86/hvm/emulate.c
@@ -160,7 +160,6 @@ void hvmemul_cancel(struct vcpu *v)
hvio->mmio_insn_bytes = 0;
hvio->mmio_access = (struct npfec){};
hvio->mmio_retry = false;
- hvio->g2m_ioport = NULL;
hvmemul_cache_disable(v);
}
--- a/xen/arch/x86/hvm/hvm.c
+++ b/xen/arch/x86/hvm/hvm.c
@@ -613,6 +613,7 @@ int hvm_domain_initialise(struct domain
spin_lock_init(&d->arch.hvm.irq_lock);
spin_lock_init(&d->arch.hvm.write_map.lock);
+ rwlock_init(&d->arch.hvm.g2m_ioport_lock);
rwlock_init(&d->arch.hvm.mmcfg_lock);
INIT_LIST_HEAD(&d->arch.hvm.write_map.list);
INIT_LIST_HEAD(&d->arch.hvm.g2m_ioport_list);
--- a/xen/arch/x86/hvm/io.c
+++ b/xen/arch/x86/hvm/io.c
@@ -143,36 +143,56 @@ bool handle_pio(uint16_t port, unsigned
return true;
}
-static bool cf_check g2m_portio_accept(
- const struct hvm_io_handler *handler, const ioreq_t *p)
+/* NB: Returns with the lock held in the success case. */
+static const struct g2m_ioport *g2m_portio_find_and_lock(struct hvm_domain *hvm,
+ uint64_t addr,
+ uint32_t size)
{
- struct vcpu *curr = current;
- const struct hvm_domain *hvm = &curr->domain->arch.hvm;
- struct hvm_vcpu_io *hvio = &curr->arch.hvm.hvm_io;
- struct g2m_ioport *g2m_ioport;
- unsigned int start, end;
+ const struct g2m_ioport *g2m_ioport;
+
+ read_lock(&hvm->g2m_ioport_lock);
list_for_each_entry( g2m_ioport, &hvm->g2m_ioport_list, list )
{
- start = g2m_ioport->gport;
- end = start + g2m_ioport->np;
- if ( (p->addr >= start) && (p->addr + p->size <= end) )
- {
- hvio->g2m_ioport = g2m_ioport;
- return 1;
- }
+ unsigned int start = g2m_ioport->gport;
+
+ if ( addr >= start && addr + size <= start + g2m_ioport->np )
+ return g2m_ioport;
}
- return 0;
+ read_unlock(&hvm->g2m_ioport_lock);
+
+ return NULL;
+}
+
+static bool cf_check g2m_portio_accept(
+ const struct hvm_io_handler *handler, const ioreq_t *p)
+{
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, p->addr, p->size);
+
+ if ( !g2m_ioport )
+ return false;
+
+ read_unlock(&hvm->g2m_ioport_lock);
+
+ return true;
}
static int cf_check g2m_portio_read(
const struct hvm_io_handler *handler, uint64_t addr, uint32_t size,
uint64_t *data)
{
- struct hvm_vcpu_io *hvio = ¤t->arch.hvm.hvm_io;
- const struct g2m_ioport *g2m_ioport = hvio->g2m_ioport;
- unsigned int mport = (addr - g2m_ioport->gport) + g2m_ioport->mport;
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, addr, size);
+ unsigned int mport;
+
+ if ( !g2m_ioport )
+ return X86EMUL_RETRY;
+
+ mport = addr - g2m_ioport->gport + g2m_ioport->mport;
switch ( size )
{
@@ -189,6 +209,8 @@ static int cf_check g2m_portio_read(
BUG();
}
+ read_unlock(&hvm->g2m_ioport_lock);
+
return X86EMUL_OKAY;
}
@@ -196,9 +218,15 @@ static int cf_check g2m_portio_write(
const struct hvm_io_handler *handler, uint64_t addr, uint32_t size,
uint64_t data)
{
- struct hvm_vcpu_io *hvio = ¤t->arch.hvm.hvm_io;
- const struct g2m_ioport *g2m_ioport = hvio->g2m_ioport;
- unsigned int mport = (addr - g2m_ioport->gport) + g2m_ioport->mport;
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, addr, size);
+ unsigned int mport;
+
+ if ( !g2m_ioport )
+ return X86EMUL_RETRY;
+
+ mport = addr - g2m_ioport->gport + g2m_ioport->mport;
switch ( size )
{
@@ -215,6 +243,8 @@ static int cf_check g2m_portio_write(
BUG();
}
+ read_unlock(&hvm->g2m_ioport_lock);
+
return X86EMUL_OKAY;
}
--- a/xen/arch/x86/include/asm/hvm/domain.h
+++ b/xen/arch/x86/include/asm/hvm/domain.h
@@ -124,6 +124,7 @@ struct hvm_domain {
/* List of guest to machine IO ports mapping. */
struct list_head g2m_ioport_list;
+ rwlock_t g2m_ioport_lock;
/* List of MMCFG regions trapped by Xen. */
struct list_head mmcfg_regions;
--- a/xen/arch/x86/include/asm/hvm/vcpu.h
+++ b/xen/arch/x86/include/asm/hvm/vcpu.h
@@ -53,8 +53,6 @@ struct hvm_vcpu_io {
unsigned long msix_unmask_address;
unsigned long msix_snoop_address;
unsigned long msix_snoop_gpa;
-
- const struct g2m_ioport *g2m_ioport;
};
struct nestedvcpu {
From: Jan Beulich <jbeulich@suse.com>
Subject: x86/HVM: add locking to I/O port translation list traversal
XEN_DOMCTL_ioport_mapping is usable by DM stubdoms, and hence we can't
assume the list to be left unaltered while the guest (really: the
hypervisor on behalf of the guest) is accessing it.
This is XSA-491 / CVE-2026-42487.
Fixes: 192c4dabc344 ("domctl and p2m changes for PCI passthru")
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Reviewed-by: Roger Pau Monné <roger.pau@citrix.com>
--- a/xen/arch/x86/domctl.c
+++ b/xen/arch/x86/domctl.c
@@ -663,6 +663,7 @@ long arch_do_domctl(
"ioport_map:add: dom%d gport=%x mport=%x nr=%x\n",
d->domain_id, fgp, fmp, np);
+ write_lock(&hvm->g2m_ioport_lock);
list_for_each_entry(g2m_ioport, &hvm->g2m_ioport_list, list)
if (g2m_ioport->mport == fmp )
{
@@ -684,11 +685,14 @@ long arch_do_domctl(
g2m_ioport->np = np;
list_add_tail(&g2m_ioport->list, &hvm->g2m_ioport_list);
}
+ write_unlock(&hvm->g2m_ioport_lock);
if ( !ret )
ret = ioports_permit_access(d, fmp, fmp + np - 1);
if ( ret && !found && g2m_ioport )
{
+ write_lock(&hvm->g2m_ioport_lock);
list_del(&g2m_ioport->list);
+ write_unlock(&hvm->g2m_ioport_lock);
xfree(g2m_ioport);
}
}
@@ -697,6 +701,8 @@ long arch_do_domctl(
printk(XENLOG_G_INFO
"ioport_map:remove: dom%d gport=%x mport=%x nr=%x\n",
d->domain_id, fgp, fmp, np);
+
+ write_lock(&hvm->g2m_ioport_lock);
list_for_each_entry(g2m_ioport, &hvm->g2m_ioport_list, list)
if ( g2m_ioport->mport == fmp )
{
@@ -704,6 +710,8 @@ long arch_do_domctl(
xfree(g2m_ioport);
break;
}
+ write_unlock(&hvm->g2m_ioport_lock);
+
ret = ioports_deny_access(d, fmp, fmp + np - 1);
if ( ret && is_hardware_domain(currd) )
printk(XENLOG_ERR
--- a/xen/arch/x86/hvm/emulate.c
+++ b/xen/arch/x86/hvm/emulate.c
@@ -160,7 +160,6 @@ void hvmemul_cancel(struct vcpu *v)
hvio->mmio_insn_bytes = 0;
hvio->mmio_access = (struct npfec){};
hvio->mmio_retry = false;
- hvio->g2m_ioport = NULL;
hvmemul_cache_disable(v);
}
--- a/xen/arch/x86/hvm/hvm.c
+++ b/xen/arch/x86/hvm/hvm.c
@@ -610,6 +610,7 @@ int hvm_domain_initialise(struct domain
spin_lock_init(&d->arch.hvm.irq_lock);
spin_lock_init(&d->arch.hvm.uc_lock);
spin_lock_init(&d->arch.hvm.write_map.lock);
+ rwlock_init(&d->arch.hvm.g2m_ioport_lock);
rwlock_init(&d->arch.hvm.mmcfg_lock);
INIT_LIST_HEAD(&d->arch.hvm.write_map.list);
INIT_LIST_HEAD(&d->arch.hvm.g2m_ioport_list);
--- a/xen/arch/x86/hvm/io.c
+++ b/xen/arch/x86/hvm/io.c
@@ -143,36 +143,56 @@ bool handle_pio(uint16_t port, unsigned
return true;
}
-static bool cf_check g2m_portio_accept(
- const struct hvm_io_handler *handler, const ioreq_t *p)
+/* NB: Returns with the lock held in the success case. */
+static const struct g2m_ioport *g2m_portio_find_and_lock(struct hvm_domain *hvm,
+ uint64_t addr,
+ uint32_t size)
{
- struct vcpu *curr = current;
- const struct hvm_domain *hvm = &curr->domain->arch.hvm;
- struct hvm_vcpu_io *hvio = &curr->arch.hvm.hvm_io;
- struct g2m_ioport *g2m_ioport;
- unsigned int start, end;
+ const struct g2m_ioport *g2m_ioport;
+
+ read_lock(&hvm->g2m_ioport_lock);
list_for_each_entry( g2m_ioport, &hvm->g2m_ioport_list, list )
{
- start = g2m_ioport->gport;
- end = start + g2m_ioport->np;
- if ( (p->addr >= start) && (p->addr + p->size <= end) )
- {
- hvio->g2m_ioport = g2m_ioport;
- return 1;
- }
+ unsigned int start = g2m_ioport->gport;
+
+ if ( addr >= start && addr + size <= start + g2m_ioport->np )
+ return g2m_ioport;
}
- return 0;
+ read_unlock(&hvm->g2m_ioport_lock);
+
+ return NULL;
+}
+
+static bool cf_check g2m_portio_accept(
+ const struct hvm_io_handler *handler, const ioreq_t *p)
+{
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, p->addr, p->size);
+
+ if ( !g2m_ioport )
+ return false;
+
+ read_unlock(&hvm->g2m_ioport_lock);
+
+ return true;
}
static int cf_check g2m_portio_read(
const struct hvm_io_handler *handler, uint64_t addr, uint32_t size,
uint64_t *data)
{
- struct hvm_vcpu_io *hvio = ¤t->arch.hvm.hvm_io;
- const struct g2m_ioport *g2m_ioport = hvio->g2m_ioport;
- unsigned int mport = (addr - g2m_ioport->gport) + g2m_ioport->mport;
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, addr, size);
+ unsigned int mport;
+
+ if ( !g2m_ioport )
+ return X86EMUL_RETRY;
+
+ mport = addr - g2m_ioport->gport + g2m_ioport->mport;
switch ( size )
{
@@ -189,6 +209,8 @@ static int cf_check g2m_portio_read(
BUG();
}
+ read_unlock(&hvm->g2m_ioport_lock);
+
return X86EMUL_OKAY;
}
@@ -196,9 +218,15 @@ static int cf_check g2m_portio_write(
const struct hvm_io_handler *handler, uint64_t addr, uint32_t size,
uint64_t data)
{
- struct hvm_vcpu_io *hvio = ¤t->arch.hvm.hvm_io;
- const struct g2m_ioport *g2m_ioport = hvio->g2m_ioport;
- unsigned int mport = (addr - g2m_ioport->gport) + g2m_ioport->mport;
+ struct hvm_domain *hvm = ¤t->domain->arch.hvm;
+ const struct g2m_ioport *g2m_ioport =
+ g2m_portio_find_and_lock(hvm, addr, size);
+ unsigned int mport;
+
+ if ( !g2m_ioport )
+ return X86EMUL_RETRY;
+
+ mport = addr - g2m_ioport->gport + g2m_ioport->mport;
switch ( size )
{
@@ -215,6 +243,8 @@ static int cf_check g2m_portio_write(
BUG();
}
+ read_unlock(&hvm->g2m_ioport_lock);
+
return X86EMUL_OKAY;
}
--- a/xen/arch/x86/include/asm/hvm/domain.h
+++ b/xen/arch/x86/include/asm/hvm/domain.h
@@ -125,6 +125,7 @@ struct hvm_domain {
/* List of guest to machine IO ports mapping. */
struct list_head g2m_ioport_list;
+ rwlock_t g2m_ioport_lock;
/* List of MMCFG regions trapped by Xen. */
struct list_head mmcfg_regions;
--- a/xen/arch/x86/include/asm/hvm/vcpu.h
+++ b/xen/arch/x86/include/asm/hvm/vcpu.h
@@ -54,8 +54,6 @@ struct hvm_vcpu_io {
unsigned long msix_unmask_address;
unsigned long msix_snoop_address;
unsigned long msix_snoop_gpa;
-
- const struct g2m_ioport *g2m_ioport;
};
struct nestedvcpu {
© 2016 - 2026 Red Hat, Inc.