[PATCH v3 2/8] KVM: add PCI MSI hypercall for VFIO passthrough
From: Jose Ignacio Tornos Martinez
Date: Thu Oct 29 2026 - 12:43:58 EST
Some PCI devices (e.g. Qualcomm WiFi adapters) have firmware that
programs MSI addresses directly into device BAR registers, bypassing
the standard PCI config MSI path. When these devices are passed
through to VMs via VFIO, the firmware programs the guest virtual MSI
address instead of the host physical IRTE address, causing DMAR faults
and non-functional interrupts.
Add a KVM hypercall (KVM_HC_PCI_MSI_ALLOC) that allows a guest to
request the host's physical MSI address/data for a VFIO-assigned PCI
device. The hypercall is dispatched from the x86 hypercall handler to
a VFIO backend that:
1. Finds the host PCI device matching the guest's vendor:device ID
using vfio_file_find_pci_device() (introduced in a previous patch)
2. Reads the host MSI address/data from the device's msi_desc or
by walking the IRQ domain compose chain
3. Returns the result to the guest via a GPA response buffer
The VFIO handler is connected to the KVM instance via a function
pointer (pci_msi_alloc) in struct kvm, set when the VFIO device is
created and cleared on release. This avoids a hard dependency between
KVM and VFIO subsystems.
When MSI is not yet enabled by QEMU, or the IRQ vector is not
configured, the handler returns success with a zeroed response,
allowing the guest to poll until the host MSI is ready.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@xxxxxxxxxx>
---
v3: new
v2: https://lore.kernel.org/all/20261011074514.515894-1-jtornosm@xxxxxxxxxx/
arch/x86/kvm/x86.c | 39 ++++++++++++++
include/linux/kvm_host.h | 3 ++
include/uapi/linux/kvm_para.h | 1 +
virt/kvm/vfio.c | 97 +++++++++++++++++++++++++++++++++++
virt/kvm/vfio.h | 8 +++
5 files changed, 148 insertions(+)
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 32563a91fba9..2f0cae07f978 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -86,6 +86,7 @@
#include <asm/sgx.h>
#include <asm/virt.h>
+#include <linux/msi.h>
#include <clocksource/hyperv_timer.h>
#define CREATE_TRACE_POINTS
@@ -7490,6 +7491,44 @@ int ____kvm_emulate_hypercall(struct kvm_vcpu *vcpu, int cpl,
vcpu->arch.complete_userspace_io = complete_hypercall;
return 0;
}
+ case KVM_HC_PCI_MSI_ALLOC: {
+ struct msi_msg msg = {};
+ struct {
+ u32 address_lo;
+ u32 address_hi;
+ u32 data;
+ u32 reserved;
+ } resp;
+
+ if (vcpu->kvm->pci_msi_alloc == NULL) {
+ ret = -KVM_ENOSYS;
+ break;
+ }
+
+ /*
+ * a0 = PCI vendor:device (vendor << 16 | device)
+ * a1 = MSI vector index
+ * a2 = GPA of response buffer
+ */
+ ret = vcpu->kvm->pci_msi_alloc(vcpu->kvm, a0, a1, &msg);
+ if (ret) {
+ ret = -KVM_EINVAL;
+ break;
+ }
+
+ resp.address_lo = msg.address_lo;
+ resp.address_hi = msg.address_hi;
+ resp.data = msg.data;
+ resp.reserved = 0;
+
+ if (kvm_write_guest(vcpu->kvm, a2, &resp, sizeof(resp))) {
+ ret = -KVM_EFAULT;
+ break;
+ }
+
+ ret = 0;
+ break;
+ }
default:
ret = -KVM_ENOSYS;
break;
diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h
index e7fc94e2910a..4921810e9f3f 100644
--- a/include/linux/kvm_host.h
+++ b/include/linux/kvm_host.h
@@ -871,6 +871,9 @@ struct kvm {
struct xarray mem_attr_array;
#endif
char stats_id[KVM_STATS_NAME_SIZE];
+
+ int (*pci_msi_alloc)(struct kvm *kvm, u32 devid,
+ unsigned int vector, struct msi_msg *msg);
};
#define kvm_err(fmt, ...) \
diff --git a/include/uapi/linux/kvm_para.h b/include/uapi/linux/kvm_para.h
index 960c7e93d1a9..3c7ca359faa6 100644
--- a/include/uapi/linux/kvm_para.h
+++ b/include/uapi/linux/kvm_para.h
@@ -30,6 +30,7 @@
#define KVM_HC_SEND_IPI 10
#define KVM_HC_SCHED_YIELD 11
#define KVM_HC_MAP_GPA_RANGE 12
+#define KVM_HC_PCI_MSI_ALLOC 13
/*
* hypercalls use architecture specific
diff --git a/virt/kvm/vfio.c b/virt/kvm/vfio.c
index 19548a430942..60356173e69f 100644
--- a/virt/kvm/vfio.c
+++ b/virt/kvm/vfio.c
@@ -11,7 +11,10 @@
#include <linux/kvm_host.h>
#include <linux/list.h>
#include <linux/module.h>
+#include <linux/irq.h>
+#include <linux/msi.h>
#include <linux/mutex.h>
+#include <linux/pci.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/vfio.h>
@@ -315,6 +318,8 @@ static void kvm_vfio_release(struct kvm_device *dev)
kvm_vfio_update_coherency(dev);
+ dev->kvm->pci_msi_alloc = NULL;
+
kfree(kv);
kfree(dev); /* alloc by kvm_ioctl_create_device, free by .release */
}
@@ -349,6 +354,98 @@ static int kvm_vfio_create(struct kvm_device *dev, u32 type)
mutex_init(&kv->lock);
dev->private = kv;
+ dev->kvm->pci_msi_alloc = kvm_vfio_pci_msi_alloc;
+
+ return 0;
+}
+
+/**
+ * kvm_vfio_pci_msi_alloc - Get physical MSI address/data for a VFIO device
+ * @kvm: the VM instance
+ * @devid: PCI vendor:device packed as (vendor << 16 | device)
+ * @vector: MSI vector index
+ * @msg: output MSI message (address + data)
+ *
+ * Walks VFIO files attached to this VM, uses vfio_file_find_pci_device()
+ * to find the PCI device matching the given vendor:device ID, and returns
+ * the host's physical MSI address/data for the requested vector.
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int kvm_vfio_pci_msi_alloc(struct kvm *kvm, u32 devid,
+ unsigned int vector, struct msi_msg *msg)
+{
+ struct kvm_vfio_file *kvf;
+ struct kvm_vfio *kv = NULL;
+ struct vfio_device *vdev;
+ struct kvm_device *dev;
+ struct msi_desc *desc;
+ struct irq_data *irqd;
+ struct pci_dev *pdev;
+ u16 vendor = devid >> 16;
+ u16 device = devid & 0xffff;
+ int irq;
+
+ scoped_guard(mutex, &kvm->lock) {
+ list_for_each_entry(dev, &kvm->devices, vm_node) {
+ if (dev->ops == &kvm_vfio_ops) {
+ kv = dev->private;
+ break;
+ }
+ }
+ }
+
+ if (!kv)
+ return -ENODEV;
+
+ guard(mutex)(&kv->lock);
+
+ vdev = NULL;
+ list_for_each_entry(kvf, &kv->file_list, node) {
+ vdev = vfio_file_find_pci_device(kvf->file, vendor, device);
+ if (vdev)
+ break;
+ }
+
+ if (!vdev)
+ return -ENODEV;
+
+ pdev = to_pci_dev(vdev->dev);
+
+ if (!pdev->msi_enabled && !pdev->msix_enabled) {
+ memset(msg, 0, sizeof(*msg));
+ return 0;
+ }
+
+ irq = pci_irq_vector(pdev, vector);
+ if (irq <= 0) {
+ memset(msg, 0, sizeof(*msg));
+ return 0;
+ }
+
+ irqd = irq_get_irq_data(irq);
+ if (!irqd) {
+ memset(msg, 0, sizeof(*msg));
+ return 0;
+ }
+
+ desc = irq_get_msi_desc(irq);
+ if (desc) {
+ *msg = desc->msg;
+ } else {
+ struct irq_data *pos;
+
+ for (pos = irqd; pos; pos = pos->parent_data) {
+ if (pos->chip && pos->chip->irq_compose_msi_msg) {
+ pos->chip->irq_compose_msi_msg(pos, msg);
+ break;
+ }
+ }
+ if (!pos) {
+ memset(msg, 0, sizeof(*msg));
+ return 0;
+ }
+ }
return 0;
}
diff --git a/virt/kvm/vfio.h b/virt/kvm/vfio.h
index e130a4a03530..c2237ca9ff38 100644
--- a/virt/kvm/vfio.h
+++ b/virt/kvm/vfio.h
@@ -5,6 +5,8 @@
#ifdef CONFIG_KVM_VFIO
int kvm_vfio_ops_init(void);
void kvm_vfio_ops_exit(void);
+int kvm_vfio_pci_msi_alloc(struct kvm *kvm, u32 devid,
+ unsigned int vector, struct msi_msg *msg);
#else
static inline int kvm_vfio_ops_init(void)
{
@@ -13,6 +15,12 @@ static inline int kvm_vfio_ops_init(void)
static inline void kvm_vfio_ops_exit(void)
{
}
+static inline int kvm_vfio_pci_msi_alloc(struct kvm *kvm, u32 devid,
+ unsigned int vector,
+ struct msi_msg *msg)
+{
+ return -ENODEV;
+}
#endif
#endif
--
2.56.0