* [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs
@ 2026-09-30 14:08 Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 1/7] PCI: Add pci_find_free_ext_cap_offset() helper Jose Ignacio Tornos Martinez
` (6 more replies)
0 siblings, 7 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel
This series enables Qualcomm ath11k/ath12k WiFi devices to work in VM
passthrough scenarios, addressing a long-standing issue where these
devices fail to initialize when passed through to VMs via VFIO.
Qualcomm ath11k/ath12k WiFi devices have been broken in VM passthrough
since they were introduced. The devices use an embedded interrupt
controller that requires physical host MSI addresses programmed to
device registers, but in VMs the driver only sees virtualized guest
addresses. This causes firmware initialization to fail with errors like
"BHI offset: 0xffffffff is out of range".
Multiple attempts have been made to solve this over the past 2 years:
March 2024: Workaround using module parameters [1]
- Required manually copying MSI values from host to VM
- Rejected by Johannes Berg as too manual, suggested VMM solution
August 2024: QEMU + kernel solution by Alex Williamson [2]
- Kernel disables MSI virtualization, QEMU intercepts config writes
- Uses brute force pattern matching to infer MSI data writes
- Stuck as RFC - no progress in 8+ months
No action from Qualcomm: Despite multiple reports, no vendor solution
The solution in this series provides a clean, kernel-only solution that
addresses the previous concerns.
Architecture:
1. qcom-vfio-pci variant driver caches physical host MSI values and
exposes them via extended config space with magic signature "QMSI"
In this way, device-specific hardware quirks belong only in kernel
drivers, not userspace, independent of the tools used.
This follows the established VFIO variant driver pattern used by
other drivers like mlx5-vfio-pci (netdev tree) and nvgrace-gpu (platform
tree).
2. VM drivers (ath11k/ath12k) automatically discover and use cached values
3. PCIe link recovery handles VM timing variations
Thoroughly tested: 3 VMs across 2 WiFi generations, all devices successfully
initialized and are ready for use.
- VM1: ath11k (WCN6855)
- VM2: ath11k (WCN6855)
- VM3: ath12k (WCN7850)
[1] https://lore.kernel.org/all/20240322104912.94811-1-jtornosm@redhat.com/
[2] https://lore.kernel.org/kvm/20240812170014.1583783-1-alex.williamson@redhat.com/
[3] Original problem report:
https://lore.kernel.org/all/fc6bd06f-d52b-4dee-ab1b-4bb845cc0b95@quicinc.com/
Jose Ignacio Tornos Martinez (7):
PCI: Add pci_find_free_ext_cap_offset() helper
vfio: Add qcom_vfio.h header for MSI cache protocol
vfio/pci: Add qcom-vfio-pci variant driver
ath11k: Add PCIe link recovery retry for VMs
ath11k: Use VFIO MSI cache when available
ath12k: Add PCIe link recovery retry for VMs
ath12k: Use VFIO MSI cache when available
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 1/7] PCI: Add pci_find_free_ext_cap_offset() helper
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 2/7] vfio: Add qcom_vfio.h header for MSI cache protocol Jose Ignacio Tornos Martinez
` (5 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
Add helper function to find free space in PCI extended configuration
space. This is needed by VFIO variant drivers that need to use unused
extended config space for device-specific purposes, such as passing
metadata to VMs.
The function scans the extended capability chain and returns an offset
to a gap of at least the requested size. This allows drivers to safely
use extended config space without conflicting with existing capabilities.
Use case: The qcom-vfio-pci variant driver uses this to find space for
caching host MSI addresses that need to be passed to Qualcomm device
firmware in VMs.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/pci/pci.c | 54 +++++++++++++++++++++++++++++++++++++++++++++
include/linux/pci.h | 3 +++
2 files changed, 57 insertions(+)
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index 8f7cfcc00090..7fa261140457 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -552,6 +552,60 @@ u16 pci_find_ext_capability(struct pci_dev *dev, int cap)
}
EXPORT_SYMBOL_GPL(pci_find_ext_capability);
+/**
+ * pci_find_free_ext_cap_offset - Find free space in extended config
+ * @dev: PCI device to query
+ * @size: Minimum size needed (in bytes)
+ *
+ * Scans the extended capability chain to find a gap of at least @size bytes
+ * in the extended configuration space (0x100-0xFFF). This can be used by
+ * drivers that need to use unused extended config space for device-specific
+ * purposes (e.g., VFIO quirks for VM passthrough).
+ *
+ * Returns the offset to free space, or 0 if no suitable gap found.
+ */
+int pci_find_free_ext_cap_offset(struct pci_dev *dev, size_t size)
+{
+ int pos = PCI_CFG_SPACE_SIZE;
+ int last_cap_start = 0;
+ int free_offset;
+ u32 header;
+ int next;
+
+ if (!pci_is_pcie(dev))
+ return 0;
+
+ while (pos >= PCI_CFG_SPACE_SIZE) {
+ if (pci_read_config_dword(dev, pos, &header))
+ break;
+
+ if (header == 0 || header == 0xffffffff)
+ break;
+
+ last_cap_start = pos;
+ next = PCI_EXT_CAP_NEXT(header);
+ if (!next)
+ break;
+ pos = next;
+ }
+
+ /*
+ * Use space after the last capability. We align to the next capability
+ * boundary (typically 4-byte aligned). Start at a safe offset past the
+ * last capability - capabilities vary in size, so use a conservative
+ * offset. Most extended capabilities are <= 64 bytes.
+ */
+ if (last_cap_start) {
+ free_offset = last_cap_start + 64;
+ free_offset = ALIGN(free_offset, 4);
+ if (PCI_CFG_SPACE_EXP_SIZE - free_offset >= size)
+ return free_offset;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(pci_find_free_ext_cap_offset);
+
/**
* pci_get_dsn - Read and return the 8-byte Device Serial Number
* @dev: PCI device to query
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 2c4454583c11..5d1ccbb2ed29 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1287,6 +1287,7 @@ u8 pci_find_ht_capability(struct pci_dev *dev, int ht_cap);
u8 pci_find_next_ht_capability(struct pci_dev *dev, u8 pos, int ht_cap);
u16 pci_find_ext_capability(struct pci_dev *dev, int cap);
u16 pci_find_next_ext_capability(struct pci_dev *dev, u16 pos, int cap);
+int pci_find_free_ext_cap_offset(struct pci_dev *dev, size_t size);
struct pci_bus *pci_find_next_bus(const struct pci_bus *from);
u16 pci_find_vsec_capability(struct pci_dev *dev, u16 vendor, int cap);
u16 pci_find_dvsec_capability(struct pci_dev *dev, u16 vendor, u16 dvsec);
@@ -2160,6 +2161,8 @@ static inline u8 pci_find_next_capability(struct pci_dev *dev, u8 post, int cap)
{ return 0; }
static inline u16 pci_find_ext_capability(struct pci_dev *dev, int cap)
{ return 0; }
+static inline int pci_find_free_ext_cap_offset(struct pci_dev *dev, size_t size)
+{ return 0; }
static inline u64 pci_get_dsn(struct pci_dev *dev)
{ return 0; }
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 2/7] vfio: Add qcom_vfio.h header for MSI cache protocol
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 1/7] PCI: Add pci_find_free_ext_cap_offset() helper Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver Jose Ignacio Tornos Martinez
` (4 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
Add shared header defining the communication protocol between
qcom-vfio-pci variant driver and Qualcomm device drivers (ath11k,
ath12k) for passing physical host MSI addresses to VMs.
Qualcomm device firmware requires physical host MSI addresses rather
than guest IOVA addresses. The qcom-vfio-pci driver caches host MSI
values in extended PCI config space with a magic signature "QMSI" for
VM discovery.
Protocol:
1. Host: qcom-vfio-pci finds free space in extended config (0x100-0xFFF)
2. Host: Writes magic signature "QMSI" (0x49534D51) followed by MSI data
3. VM: Device driver searches for "QMSI" signature to locate cached MSI
4. VM: Reads MSI values and programs device firmware
This avoids offset mismatch when QEMU filters certain capabilities,
causing host and VM to see different extended capability chains.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
include/linux/qcom_vfio.h | 88 +++++++++++++++++++++++++++++++++++++++
1 file changed, 88 insertions(+)
create mode 100644 include/linux/qcom_vfio.h
diff --git a/include/linux/qcom_vfio.h b/include/linux/qcom_vfio.h
new file mode 100644
index 000000000000..c59c81936e84
--- /dev/null
+++ b/include/linux/qcom_vfio.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-FileCopyrightText: Copyright Red Hat */
+
+#ifndef _QCOM_VFIO_H_
+#define _QCOM_VFIO_H_
+
+#include <linux/pci.h>
+
+/*
+ * MSI Address Passthrough Quirk
+ *
+ * Qualcomm device firmware requires physical host MSI addresses rather than
+ * guest IOVA addresses. The qcom-vfio-pci variant driver caches host MSI
+ * values in extended PCI config space with a magic signature for VM
+ * discovery.
+ *
+ * Protocol:
+ * 1. Host: qcom-vfio-pci finds free space in extended config (0x100-0xFFF)
+ * 2. Host: Writes magic signature "QMSI" (0x49534D51) followed by MSI data
+ * 3. VM: Device driver searches for "QMSI" signature to locate cached MSI
+ * 4. VM: Reads MSI values and programs device firmware
+ *
+ * This avoids offset mismatch when QEMU filters certain capabilities, causing
+ * host and VM to see different extended capability chains.
+ *
+ * Layout in extended config space:
+ * +0x00: Magic signature "QMSI" (0x49534D51)
+ * +0x04: MSI address low 32 bits
+ * +0x08: MSI address high 32 bits
+ * +0x0C: MSI data (16 bits)
+ *
+ * Applicable to:
+ * - ath11k WiFi (PCI ID 0x17cb:0x1103)
+ * - ath12k WiFi (PCI ID 0x17cb:0x1107)
+ */
+
+/* Magic signature "QMSI" to identify cached MSI data */
+#define QCOM_VFIO_MSI_MAGIC 0x49534D51
+
+/* Space needed for magic + MSI passthrough data */
+#define QCOM_VFIO_MSI_SPACE_SIZE 16
+
+/* Relative offsets from base discovered by qcom_vfio_find_msi_offset() */
+#define QCOM_VFIO_MSI_MAGIC_OFFSET 0x00 /* Magic signature "QMSI" */
+#define QCOM_VFIO_MSI_ADDR_LO_OFFSET 0x04 /* Host MSI address low 32 bits */
+#define QCOM_VFIO_MSI_ADDR_HI_OFFSET 0x08 /* Host MSI address high 32 bits */
+#define QCOM_VFIO_MSI_DATA_OFFSET 0x0C /* Host MSI data (16 bits) */
+
+/**
+ * qcom_vfio_find_msi_offset - Find free space for host to cache MSI
+ * @pdev: PCI device
+ *
+ * Host-side function: Finds free space in extended config to write MSI cache.
+ * Uses PCI core utility to scan capability chain.
+ *
+ * Returns: Offset in extended config space (>= 0x100), or 0 if no space found
+ */
+static inline int qcom_vfio_find_msi_offset(struct pci_dev *pdev)
+{
+ return pci_find_free_ext_cap_offset(pdev, QCOM_VFIO_MSI_SPACE_SIZE);
+}
+
+/**
+ * qcom_vfio_find_msi_cache - Locate cached MSI data by magic signature
+ * @pdev: PCI device
+ *
+ * VM-side function: Searches extended config space for "QMSI" magic signature
+ * to locate host-cached MSI values. This avoids offset mismatch when QEMU
+ * filters certain capabilities.
+ *
+ * Returns: Offset of MSI data (after magic), or 0 if not found
+ */
+static inline int qcom_vfio_find_msi_cache(struct pci_dev *pdev)
+{
+ u32 magic;
+ int offset;
+
+ for (offset = 0x100; offset <= 0xf00; offset += 4) {
+ if (pci_read_config_dword(pdev, offset, &magic))
+ continue;
+ if (magic == QCOM_VFIO_MSI_MAGIC)
+ return offset;
+ }
+
+ return 0;
+}
+
+#endif /* _QCOM_VFIO_H_ */
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 1/7] PCI: Add pci_find_free_ext_cap_offset() helper Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 2/7] vfio: Add qcom_vfio.h header for MSI cache protocol Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 15:12 ` Jason Gunthorpe
2026-09-30 14:08 ` [PATCH 4/7] ath11k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
` (3 subsequent siblings)
6 siblings, 1 reply; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
Add VFIO variant driver for Qualcomm PCIe devices that require
MSI address passthrough for VM operation.
Qualcomm ath11k and ath12k WiFi devices have embedded interrupt
controllers that require physical host MSI addresses programmed to
device registers. In VMs, the driver only sees virtualized guest
addresses, causing firmware initialization to fail.
This variant driver:
1. Caches physical host MSI values after allocation
2. Writes them to extended config space with magic signature "QMSI"
3. VM drivers discover and use these values automatically
The driver follows the VFIO variant driver pattern used by mlx5-vfio-pci
and nvgrace-gpu, providing device-specific quirks within the kernel
without requiring QEMU modifications.
Note: the VM must have IOMMU interrupt remapping and caching mode
enabled so that MSI writes from the device using cached host physical
addresses are properly routed through the IOMMU interrupt remapping
table to the VM.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/vfio/pci/Kconfig | 2 +
drivers/vfio/pci/Makefile | 2 +
drivers/vfio/pci/qcom/Kconfig | 22 ++
drivers/vfio/pci/qcom/Makefile | 3 +
drivers/vfio/pci/qcom/main.c | 272 ++++++++++++++++++++++++++
drivers/vfio/pci/qcom/msi_quirk.c | 163 ++++++++++++++++
drivers/vfio/pci/qcom/qcom_vfio_pci.h | 36 ++++
7 files changed, 500 insertions(+)
create mode 100644 drivers/vfio/pci/qcom/Kconfig
create mode 100644 drivers/vfio/pci/qcom/Makefile
create mode 100644 drivers/vfio/pci/qcom/main.c
create mode 100644 drivers/vfio/pci/qcom/msi_quirk.c
create mode 100644 drivers/vfio/pci/qcom/qcom_vfio_pci.h
diff --git a/drivers/vfio/pci/Kconfig b/drivers/vfio/pci/Kconfig
index 296bf01e185e..237a29443bf5 100644
--- a/drivers/vfio/pci/Kconfig
+++ b/drivers/vfio/pci/Kconfig
@@ -72,6 +72,8 @@ source "drivers/vfio/pci/nvgrace-gpu/Kconfig"
source "drivers/vfio/pci/qat/Kconfig"
+source "drivers/vfio/pci/qcom/Kconfig"
+
source "drivers/vfio/pci/xe/Kconfig"
endmenu
diff --git a/drivers/vfio/pci/Makefile b/drivers/vfio/pci/Makefile
index 6138f1bf241d..63db19102850 100644
--- a/drivers/vfio/pci/Makefile
+++ b/drivers/vfio/pci/Makefile
@@ -23,4 +23,6 @@ obj-$(CONFIG_NVGRACE_GPU_VFIO_PCI) += nvgrace-gpu/
obj-$(CONFIG_QAT_VFIO_PCI) += qat/
+obj-$(CONFIG_QCOM_VFIO_PCI) += qcom/
+
obj-$(CONFIG_XE_VFIO_PCI) += xe/
diff --git a/drivers/vfio/pci/qcom/Kconfig b/drivers/vfio/pci/qcom/Kconfig
new file mode 100644
index 000000000000..8e4c170d01a0
--- /dev/null
+++ b/drivers/vfio/pci/qcom/Kconfig
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config QCOM_VFIO_PCI
+ tristate "VFIO support for Qualcomm PCI devices"
+ depends on PCI
+ select VFIO_PCI_CORE
+ help
+ This provides VFIO support for Qualcomm PCI devices with device-specific
+ quirks. Currently supports:
+ - Qualcomm ath11k WiFi (Device ID 0x1103)
+ - Qualcomm ath12k WiFi (Device ID 0x1107)
+
+ These devices require special handling for MSI address/data passthrough
+ when used with VFIO. The firmware expects physical host MSI addresses
+ rather than guest IOVA addresses.
+
+ The VM must have IOMMU interrupt remapping and caching mode enabled.
+ For example, with libvirt/QEMU on Intel, add to the domain XML:
+ <iommu model="intel">
+ <driver intremap="on" caching_mode="on"/>
+ </iommu>
+
+ If you don't know what to do here, say N.
diff --git a/drivers/vfio/pci/qcom/Makefile b/drivers/vfio/pci/qcom/Makefile
new file mode 100644
index 000000000000..dc48d87d1ef6
--- /dev/null
+++ b/drivers/vfio/pci/qcom/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_QCOM_VFIO_PCI) += qcom-vfio-pci.o
+qcom-vfio-pci-y := main.o msi_quirk.o
diff --git a/drivers/vfio/pci/qcom/main.c b/drivers/vfio/pci/qcom/main.c
new file mode 100644
index 000000000000..0abfdd5faf80
--- /dev/null
+++ b/drivers/vfio/pci/qcom/main.c
@@ -0,0 +1,272 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: Copyright Red Hat
+
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/pci.h>
+#include <linux/stddef.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/uaccess.h>
+#include <linux/vfio.h>
+#include <linux/vfio_pci_core.h>
+#include <uapi/linux/vfio.h>
+
+#include "qcom_vfio_pci.h"
+
+static struct qcom_vfio_pci_device *to_qcom_vdev(struct vfio_pci_core_device *vdev)
+{
+ return container_of(vdev, struct qcom_vfio_pci_device, vdev);
+}
+
+/*
+ * Intercept config space read operations to return cached host MSI values.
+ * Call core VFIO if not intercepted.
+ */
+static ssize_t qcom_vfio_pci_read(struct vfio_device *core_vdev, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct vfio_pci_core_device *vdev =
+ container_of(core_vdev, struct vfio_pci_core_device, vdev);
+ struct qcom_vfio_pci_device *qdev = to_qcom_vdev(vdev);
+ unsigned int index = VFIO_PCI_OFFSET_TO_INDEX(*ppos);
+ loff_t offset = *ppos & VFIO_PCI_OFFSET_MASK;
+ ssize_t ret;
+
+ if (index == VFIO_PCI_CONFIG_REGION_INDEX) {
+ ret = qcom_vfio_pci_config_rw(qdev, buf, count, offset, false);
+ if (ret != -ENOTTY) {
+ if (ret > 0)
+ *ppos += ret;
+ return ret;
+ }
+ }
+
+ return vfio_pci_core_read(core_vdev, buf, count, ppos);
+}
+
+static int qcom_vfio_pci_init_dev(struct vfio_device *core_vdev)
+{
+ struct qcom_vfio_pci_device *qdev =
+ container_of(core_vdev, struct qcom_vfio_pci_device, vdev.vdev);
+ struct pci_dev *pdev = to_pci_dev(core_vdev->dev);
+ int ret;
+
+ ret = vfio_pci_core_init_dev(core_vdev);
+ if (ret)
+ return ret;
+
+ qdev->msi_offset = qcom_vfio_find_msi_offset(pdev);
+ if (!qdev->msi_offset) {
+ pci_err(pdev, "No free space in extended config for MSI quirk\n");
+ pci_err(pdev, "qcom-vfio-pci cannot function without MSI passthrough, failing probe\n");
+ vfio_pci_core_release_dev(core_vdev);
+ return -ENODEV;
+ }
+ pci_info(pdev, "MSI passthrough quirk using offset 0x%x\n", qdev->msi_offset);
+
+ qdev->msi_cached = false;
+ qdev->msi_addr_lo = 0;
+ qdev->msi_addr_hi = 0;
+ qdev->msi_data = 0;
+
+ return 0;
+}
+
+static const struct vfio_pci_device_ops qcom_vfio_pci_device_ops = {
+ /* No device-specific operations needed */
+};
+
+static int qcom_vfio_pci_open_device(struct vfio_device *core_vdev)
+{
+ struct qcom_vfio_pci_device *qdev =
+ container_of(core_vdev, struct qcom_vfio_pci_device, vdev.vdev);
+ struct vfio_pci_core_device *vdev = &qdev->vdev;
+ int ret;
+
+ ret = vfio_pci_core_enable(vdev);
+ if (ret)
+ return ret;
+
+ vfio_pci_core_finish_enable(vdev);
+ return 0;
+}
+
+/*
+ * Wrap ioctl to intercept MSI allocation and cache host MSI values.
+ * This is called after QEMU allocates MSI via VFIO_DEVICE_SET_IRQS.
+ */
+static long qcom_vfio_pci_ioctl(struct vfio_device *core_vdev,
+ unsigned int cmd, unsigned long arg)
+{
+ struct vfio_pci_core_device *vdev =
+ container_of(core_vdev, struct vfio_pci_core_device, vdev);
+ struct qcom_vfio_pci_device *qdev = to_qcom_vdev(vdev);
+ struct vfio_irq_set hdr;
+ long ret;
+
+ ret = vfio_pci_core_ioctl(core_vdev, cmd, arg);
+
+ /*
+ * If SET_IRQS ioctl succeeded for MSI and MSI wasn't cached yet,
+ * cache the MSI descriptor values.
+ * Only intercept MSI index (not INTx, MSI-X, ERR, or REQ).
+ */
+ if (ret == 0 && cmd == VFIO_DEVICE_SET_IRQS) {
+ unsigned long minsz = offsetofend(struct vfio_irq_set, count);
+
+ if (copy_from_user(&hdr, (void __user *)arg, minsz))
+ return ret;
+
+ if (hdr.index == VFIO_PCI_MSI_IRQ_INDEX) {
+ pci_dbg(qdev->vdev.pdev,
+ "SET_IRQS: index=%u flags=0x%x start=%u count=%u cached=%d\n",
+ hdr.index, hdr.flags, hdr.start, hdr.count, qdev->msi_cached);
+
+ if (!qdev->msi_cached && (hdr.flags & VFIO_IRQ_SET_ACTION_TRIGGER))
+ qcom_cache_and_write_msi(qdev);
+ }
+ }
+
+ return ret;
+}
+
+static const struct vfio_device_ops qcom_vfio_pci_ops = {
+ .name = "qcom-vfio-pci",
+ .init = qcom_vfio_pci_init_dev,
+ .release = vfio_pci_core_release_dev,
+ .open_device = qcom_vfio_pci_open_device,
+ .close_device = vfio_pci_core_close_device,
+ .ioctl = qcom_vfio_pci_ioctl,
+ .get_region_info_caps = vfio_pci_ioctl_get_region_info,
+ .device_feature = vfio_pci_core_ioctl_feature,
+ .read = qcom_vfio_pci_read,
+ .write = vfio_pci_core_write,
+ .mmap = vfio_pci_core_mmap,
+ .request = vfio_pci_core_request,
+ .match = vfio_pci_core_match,
+ .match_token_uuid = vfio_pci_core_match_token_uuid,
+ .bind_iommufd = vfio_iommufd_physical_bind,
+ .unbind_iommufd = vfio_iommufd_physical_unbind,
+ .attach_ioas = vfio_iommufd_physical_attach_ioas,
+ .detach_ioas = vfio_iommufd_physical_detach_ioas,
+};
+
+static int qcom_vfio_pci_probe(struct pci_dev *pdev,
+ const struct pci_device_id *id)
+{
+ struct qcom_vfio_pci_device *qdev;
+ int ret;
+
+ qdev = vfio_alloc_device(qcom_vfio_pci_device, vdev.vdev,
+ &pdev->dev, &qcom_vfio_pci_ops);
+ if (IS_ERR(qdev))
+ return PTR_ERR(qdev);
+
+ dev_set_drvdata(&pdev->dev, &qdev->vdev);
+ qdev->vdev.pci_ops = &qcom_vfio_pci_device_ops;
+ ret = vfio_pci_core_register_device(&qdev->vdev);
+ if (ret)
+ goto out_put_vdev;
+
+ return 0;
+
+out_put_vdev:
+ vfio_put_device(&qdev->vdev.vdev);
+ return ret;
+}
+
+static void qcom_vfio_pci_remove(struct pci_dev *pdev)
+{
+ struct vfio_pci_core_device *vdev = dev_get_drvdata(&pdev->dev);
+ struct qcom_vfio_pci_device *qdev = to_qcom_vdev(vdev);
+
+ vfio_pci_core_unregister_device(&qdev->vdev);
+ vfio_put_device(&qdev->vdev.vdev);
+}
+
+static const struct pci_device_id qcom_vfio_pci_table[] = {
+ /* Qualcomm ath11k WiFi */
+ { PCI_DRIVER_OVERRIDE_DEVICE_VFIO(PCI_VENDOR_ID_QCOM, 0x1103) },
+ /* Qualcomm ath12k WiFi */
+ { PCI_DRIVER_OVERRIDE_DEVICE_VFIO(PCI_VENDOR_ID_QCOM, 0x1107) },
+ { }
+};
+
+MODULE_DEVICE_TABLE(pci, qcom_vfio_pci_table);
+
+static struct pci_driver qcom_vfio_pci_driver = {
+ .name = "qcom-vfio-pci",
+ .id_table = qcom_vfio_pci_table,
+ .probe = qcom_vfio_pci_probe,
+ .remove = qcom_vfio_pci_remove,
+ .driver_managed_dma = true,
+};
+
+static char ids[1024] __initdata;
+module_param_string(ids, ids, sizeof(ids), 0);
+MODULE_PARM_DESC(ids, "Initial PCI IDs to add to the driver, format is \"vendor:device[:subvendor[:subdevice[:class[:class_mask]]]]\" and multiple comma separated entries can be specified");
+
+static void __init qcom_vfio_pci_fill_ids(void)
+{
+ char *p, *id;
+ int rc;
+
+ if (ids[0] == '\0')
+ return;
+
+ p = ids;
+ while ((id = strsep(&p, ","))) {
+ unsigned int vendor, device, subvendor = PCI_ANY_ID,
+ subdevice = PCI_ANY_ID, class = 0, class_mask = 0;
+ int fields;
+
+ if (!strlen(id))
+ continue;
+
+ fields = sscanf(id, "%x:%x:%x:%x:%x:%x",
+ &vendor, &device, &subvendor, &subdevice,
+ &class, &class_mask);
+
+ if (fields < 2) {
+ pr_warn("qcom-vfio-pci: invalid id string \"%s\"\n", id);
+ continue;
+ }
+
+ rc = pci_add_dynid(&qcom_vfio_pci_driver, vendor, device,
+ subvendor, subdevice, class, class_mask, 0);
+ if (rc)
+ pr_warn("qcom-vfio-pci: failed to add dynamic id [%04x:%04x[%04x:%04x]] class %#08x/%08x (%d)\n",
+ vendor, device, subvendor, subdevice,
+ class, class_mask, rc);
+ else
+ pr_info("qcom-vfio-pci: add [%04x:%04x[%04x:%04x]] class %#08x/%08x\n",
+ vendor, device, subvendor, subdevice,
+ class, class_mask);
+ }
+}
+
+static int __init qcom_vfio_pci_init(void)
+{
+ int ret;
+
+ ret = pci_register_driver(&qcom_vfio_pci_driver);
+ if (ret)
+ return ret;
+
+ qcom_vfio_pci_fill_ids();
+
+ return 0;
+}
+
+static void __exit qcom_vfio_pci_exit(void)
+{
+ pci_unregister_driver(&qcom_vfio_pci_driver);
+}
+
+module_init(qcom_vfio_pci_init);
+module_exit(qcom_vfio_pci_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Jose Ignacio Tornos Martinez <jtornosm@redhat.com>");
+MODULE_DESCRIPTION("Qualcomm VFIO PCI - Device-Specific VFIO PCI driver for Qualcomm devices");
diff --git a/drivers/vfio/pci/qcom/msi_quirk.c b/drivers/vfio/pci/qcom/msi_quirk.c
new file mode 100644
index 000000000000..583673d699d5
--- /dev/null
+++ b/drivers/vfio/pci/qcom/msi_quirk.c
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: Copyright Red Hat
+
+#include <linux/kernel.h>
+#include <linux/msi.h>
+#include <linux/pci.h>
+#include <linux/uaccess.h>
+#include <linux/vfio_pci_core.h>
+
+#include "qcom_vfio_pci.h"
+
+/**
+ * qcom_cache_and_write_msi - Cache host MSI address and data
+ * @qdev: Qualcomm VFIO device
+ *
+ * Called after QEMU allocates MSI via SET_IRQS ioctl to cache the physical
+ * host MSI addresses that will be returned to the VM driver.
+ */
+void qcom_cache_and_write_msi(struct qcom_vfio_pci_device *qdev)
+{
+ struct pci_dev *pdev = qdev->vdev.pdev;
+ struct device *dev = &pdev->dev;
+ struct msi_desc *desc;
+ u32 addr_lo, addr_hi;
+ u16 data;
+
+ if (qdev->msi_cached)
+ return;
+
+ if (!qdev->msi_offset)
+ return;
+
+ __msi_lock_descs(dev);
+ desc = msi_first_desc(dev, MSI_DESC_ASSOCIATED);
+ if (!desc) {
+ __msi_unlock_descs(dev);
+ pci_warn(pdev, "MSI descriptor not found\n");
+ return;
+ }
+
+ addr_lo = desc->msg.address_lo;
+ addr_hi = desc->msg.address_hi;
+ data = desc->msg.data;
+ __msi_unlock_descs(dev);
+
+ pci_dbg(pdev, "MSI descriptor - addr_lo=0x%x addr_hi=0x%x data=0x%x\n",
+ addr_lo, addr_hi, data);
+
+ /*
+ * Validate: address must be valid. Data can be 0 with interrupt remapping.
+ * When Intel VT-d interrupt remapping is enabled, the MSI data field
+ * contains an index into the interrupt remapping table, which can be 0.
+ * We cache it anyway since that's what the VM driver will program.
+ */
+ if (!addr_lo) {
+ pci_warn(pdev, "MSI address invalid, skipping cache\n");
+ return;
+ }
+ qdev->msi_addr_lo = addr_lo;
+ qdev->msi_addr_hi = addr_hi;
+ qdev->msi_data = data;
+ qdev->msi_cached = true;
+
+ pci_write_config_dword(pdev, qdev->msi_offset + QCOM_VFIO_MSI_MAGIC_OFFSET,
+ QCOM_VFIO_MSI_MAGIC);
+ pci_write_config_dword(pdev, qdev->msi_offset + QCOM_VFIO_MSI_ADDR_LO_OFFSET,
+ qdev->msi_addr_lo);
+ pci_write_config_dword(pdev, qdev->msi_offset + QCOM_VFIO_MSI_ADDR_HI_OFFSET,
+ qdev->msi_addr_hi);
+ pci_write_config_word(pdev, qdev->msi_offset + QCOM_VFIO_MSI_DATA_OFFSET,
+ qdev->msi_data);
+
+ pci_info(pdev, "Qualcomm WiFi MSI cached at offset 0x%x - addr_lo=0x%x addr_hi=0x%x data=0x%x (magic=QMSI)\n",
+ qdev->msi_offset, qdev->msi_addr_lo, qdev->msi_addr_hi, qdev->msi_data);
+}
+
+/**
+ * qcom_intercept_msi_read - Intercept MSI config reads and return cached values
+ * @qdev: Qualcomm VFIO device
+ * @pos: Config space offset
+ * @buf: User buffer
+ * @count: Read size
+ *
+ * Returns: Number of bytes read, or negative error
+ */
+static ssize_t qcom_intercept_msi_read(struct qcom_vfio_pci_device *qdev,
+ loff_t pos, char __user *buf, size_t count)
+{
+ u32 tmp_val = 0;
+ int msi_magic, msi_addr_lo, msi_addr_hi, msi_data;
+
+ if (!qdev->msi_offset)
+ return -ENOTTY;
+
+ msi_magic = qdev->msi_offset + QCOM_VFIO_MSI_MAGIC_OFFSET;
+ msi_addr_lo = qdev->msi_offset + QCOM_VFIO_MSI_ADDR_LO_OFFSET;
+ msi_addr_hi = qdev->msi_offset + QCOM_VFIO_MSI_ADDR_HI_OFFSET;
+ msi_data = qdev->msi_offset + QCOM_VFIO_MSI_DATA_OFFSET;
+
+ if (pos == msi_magic && count == sizeof(u32)) {
+ if (qdev->msi_cached) {
+ tmp_val = QCOM_VFIO_MSI_MAGIC;
+ pci_dbg(qdev->vdev.pdev, "Intercepting MAGIC read at 0x%llx, returning 0x%x (QMSI)\n",
+ pos, tmp_val);
+ } else {
+ return -ENOTTY;
+ }
+ } else if (qdev->msi_cached) {
+ if (pos == msi_addr_lo && count == sizeof(u32)) {
+ tmp_val = qdev->msi_addr_lo;
+ pci_dbg(qdev->vdev.pdev, "Intercepting ADDR_LO read at 0x%llx, returning 0x%x\n",
+ pos, tmp_val);
+ } else if (pos == msi_addr_hi && count == sizeof(u32)) {
+ tmp_val = qdev->msi_addr_hi;
+ pci_dbg(qdev->vdev.pdev, "Intercepting ADDR_HI read at 0x%llx, returning 0x%x\n",
+ pos, tmp_val);
+ } else if (pos == msi_data && count == sizeof(u16)) {
+ tmp_val = qdev->msi_data;
+ pci_dbg(qdev->vdev.pdev, "Intercepting MSI_DATA read at 0x%llx, returning 0x%x\n",
+ pos, tmp_val);
+ } else {
+ return -ENOTTY;
+ }
+ } else {
+ return -ENOTTY;
+ }
+
+ if (copy_to_user(buf, &tmp_val, count))
+ return -EFAULT;
+
+ return count;
+}
+
+/**
+ * qcom_vfio_pci_config_rw - Config space read/write with MSI interception
+ * @qdev: Qualcomm VFIO device
+ * @buf: User buffer
+ * @count: Size
+ * @offset: Config space offset (0-0xFFF)
+ * @iswrite: true for write, false for read
+ *
+ * Returns: Number of bytes transferred, or -ENOTTY if not intercepted
+ */
+ssize_t qcom_vfio_pci_config_rw(struct qcom_vfio_pci_device *qdev,
+ char __user *buf, size_t count,
+ loff_t offset, bool iswrite)
+{
+ int msi_range_start, msi_range_end;
+
+ if (iswrite)
+ return -ENOTTY;
+
+ if (!qdev->msi_offset)
+ return -ENOTTY;
+
+ msi_range_start = qdev->msi_offset;
+ msi_range_end = qdev->msi_offset + QCOM_VFIO_MSI_SPACE_SIZE;
+
+ if (offset >= msi_range_start && offset < msi_range_end)
+ return qcom_intercept_msi_read(qdev, offset, buf, count);
+
+ return -ENOTTY;
+}
diff --git a/drivers/vfio/pci/qcom/qcom_vfio_pci.h b/drivers/vfio/pci/qcom/qcom_vfio_pci.h
new file mode 100644
index 000000000000..de37fd94b74b
--- /dev/null
+++ b/drivers/vfio/pci/qcom/qcom_vfio_pci.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-FileCopyrightText: Copyright Red Hat */
+
+#ifndef _QCOM_VFIO_PCI_H_
+#define _QCOM_VFIO_PCI_H_
+
+#include <linux/vfio_pci_core.h>
+#include <linux/qcom_vfio.h>
+
+/**
+ * struct qcom_vfio_pci_device - Qualcomm VFIO PCI device with quirks
+ * @vdev: Core VFIO PCI device
+ * @msi_offset: Base offset in extended config for MSI passthrough (0 if disabled)
+ * @msi_cached: Whether host MSI address/data has been cached
+ * @msi_addr_lo: Cached host MSI address (low 32 bits)
+ * @msi_addr_hi: Cached host MSI address (high 32 bits)
+ * @msi_data: Cached host MSI data value
+ */
+struct qcom_vfio_pci_device {
+ struct vfio_pci_core_device vdev;
+
+ /* MSI quirk state */
+ int msi_offset;
+ bool msi_cached;
+ u32 msi_addr_lo;
+ u32 msi_addr_hi;
+ u16 msi_data;
+};
+
+/* MSI quirk functions */
+ssize_t qcom_vfio_pci_config_rw(struct qcom_vfio_pci_device *qdev,
+ char __user *buf, size_t count,
+ loff_t offset, bool iswrite);
+void qcom_cache_and_write_msi(struct qcom_vfio_pci_device *qdev);
+
+#endif /* _QCOM_VFIO_PCI_H_ */
--
2.54.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 4/7] ath11k: add PCIe link recovery retry
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
` (2 preceding siblings ...)
2026-09-30 14:08 ` [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 5/7] ath11k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
` (2 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
When ath11k devices are passed through to VMs via PCIe passthrough,
the PCIe link may not stabilize immediately after global reset due to
virtualization layer timing variations. This causes MHI initialization
to fail with "BHI offset: 0xffffffff is out of range" errors because
the device is read before it's ready.
Currently, ath11k_pci_soc_global_reset() just warns if the link is
down but continues anyway, leading to inevitable MHI failures.
This patch adds:
1. A retry loop (up to 5000ms) to wait for the PCIe link to recover
after global reset (similar to the existing ath12k LTSSM retry logic)
2. Proper error propagation to prevent continuing with a dead link
This is especially important for VM environments where timing can vary
significantly from bare metal.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/net/wireless/ath/ath11k/pci.c | 39 ++++++++++++++++++++++-----
1 file changed, 32 insertions(+), 7 deletions(-)
diff --git a/drivers/net/wireless/ath/ath11k/pci.c b/drivers/net/wireless/ath/ath11k/pci.c
index 7114eca8810d..4fc6aaf036c1 100644
--- a/drivers/net/wireless/ath/ath11k/pci.c
+++ b/drivers/net/wireless/ath/ath11k/pci.c
@@ -190,8 +190,10 @@ static void ath11k_pci_restore_window(struct ath11k_base *ab)
spin_unlock_bh(&ab_pci->window_lock);
}
-static void ath11k_pci_soc_global_reset(struct ath11k_base *ab)
+static int ath11k_pci_soc_global_reset(struct ath11k_base *ab)
{
+ unsigned long timeout;
+ bool link_recovered = false;
u32 val, delay;
val = ath11k_pcic_read32(ab, PCIE_SOC_GLOBAL_RESET);
@@ -211,14 +213,29 @@ static void ath11k_pci_soc_global_reset(struct ath11k_base *ab)
mdelay(delay);
- val = ath11k_pcic_read32(ab, PCIE_SOC_GLOBAL_RESET);
- if (val == 0xffffffff)
- ath11k_warn(ab, "link down error during global reset\n");
+ /* Wait for PCIe link to recover, especially important in VM environments
+ * where timing can vary significantly from bare metal.
+ */
+ timeout = jiffies + msecs_to_jiffies(5000);
+ while (time_before(jiffies, timeout)) {
+ val = ath11k_pcic_read32(ab, PCIE_SOC_GLOBAL_RESET);
+ if (val != 0xffffffff) {
+ link_recovered = true;
+ break;
+ }
+ mdelay(20);
+ }
+ if (!link_recovered) {
+ ath11k_err(ab, "PCIe link failed to recover - device may need power cycle\n");
+ return -ETIMEDOUT;
+ }
/* Restore window register as its content is cleared during
* hardware global reset, such that it aligns with host cache.
*/
ath11k_pci_restore_window(ab);
+
+ return 0;
}
static void ath11k_pci_clear_dbg_registers(struct ath11k_base *ab)
@@ -373,8 +390,10 @@ static void ath11k_pci_force_wake(struct ath11k_base *ab)
mdelay(5);
}
-static void ath11k_pci_sw_reset(struct ath11k_base *ab, bool power_on)
+static int ath11k_pci_sw_reset(struct ath11k_base *ab, bool power_on)
{
+ int ret;
+
mdelay(100);
if (power_on) {
@@ -387,8 +406,12 @@ static void ath11k_pci_sw_reset(struct ath11k_base *ab, bool power_on)
ath11k_mhi_clear_vector(ab);
ath11k_pci_clear_dbg_registers(ab);
- ath11k_pci_soc_global_reset(ab);
+ ret = ath11k_pci_soc_global_reset(ab);
+ if (ret)
+ return ret;
ath11k_mhi_set_mhictrl_reset(ab);
+
+ return 0;
}
static void ath11k_pci_init_qmi_ce_config(struct ath11k_base *ab)
@@ -818,7 +841,9 @@ static int ath11k_pci_power_up(struct ath11k_base *ab)
ab_pci->register_window = 0;
clear_bit(ATH11K_FLAG_DEVICE_INIT_DONE, &ab->dev_flags);
- ath11k_pci_sw_reset(ab_pci->ab, true);
+ ret = ath11k_pci_sw_reset(ab_pci->ab, true);
+ if (ret)
+ return ret;
/* Disable ASPM during firmware download due to problems switching
* to AMSS state.
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 5/7] ath11k: Use VFIO MSI cache when available
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
` (3 preceding siblings ...)
2026-09-30 14:08 ` [PATCH 4/7] ath11k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 6/7] ath12k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 7/7] ath12k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
The Qualcomm ath11k firmware requires physical host MSI addresses for
interrupt configuration but cannot handle IOVA-based MSI addressing in
virtualized environments.
When the device is passed through to a VM via VFIO:
- The guest kernel allocates MSI vectors and sees IOVA addresses
- The ath11k firmware expects physical host MSI addresses
- Providing IOVA to the firmware causes interrupt delivery to fail
- Result: device is non-functional in VMs
The qcom-vfio-pci variant driver (introduced in earlier patches) solves
this by caching the physical host MSI values and exposing them through
PCI extended config space with a "QMSI" magic signature for discovery.
Search for the QMSI magic signature in extended config space. If found,
the device is running on a VM and we can read the cached host MSI values
from that location. If the VFIO cache is not present (i.e. on baremetal),
fall back to standard PCI MSI configuration.
This change is transparent, arch independent and requires no user
configuration when used with the qcom-vfio-pci driver.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/net/wireless/ath/ath11k/pci.c | 33 +++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/drivers/net/wireless/ath/ath11k/pci.c b/drivers/net/wireless/ath/ath11k/pci.c
index c372676b097f..99a3a709831c 100644
--- a/drivers/net/wireless/ath/ath11k/pci.c
+++ b/drivers/net/wireless/ath/ath11k/pci.c
@@ -10,6 +10,7 @@
#include <linux/of.h>
#include <linux/time.h>
#include <linux/vmalloc.h>
+#include <linux/qcom_vfio.h>
#include "pci.h"
#include "core.h"
@@ -463,8 +464,11 @@ static int ath11k_pci_alloc_msi(struct ath11k_pci *ab_pci)
struct ath11k_base *ab = ab_pci->ab;
const struct ath11k_msi_config *msi_config = ab->pci.msi.config;
struct pci_dev *pci_dev = ab_pci->pdev;
+ u32 msi_addr_lo = 0, msi_addr_hi = 0;
struct msi_desc *msi_desc;
+ u16 msi_data = 0;
int num_vectors;
+ int msi_offset;
int ret;
num_vectors = pci_alloc_irq_vectors(pci_dev,
@@ -488,6 +492,35 @@ static int ath11k_pci_alloc_msi(struct ath11k_pci *ab_pci)
}
ath11k_info(ab, "MSI vectors: %d\n", num_vectors);
+ /*
+ * Try to get host MSI address from VFIO cache.
+ * When running on a VM, the ath11k firmware requires physical host
+ * MSI addresses and cannot handle guest IOVA addresses.
+ * qcom-vfio-pci writes host MSI values to extended config space with
+ * a magic signature "QMSI" for discovery. Search for it.
+ */
+ msi_offset = qcom_vfio_find_msi_cache(pci_dev);
+ if (msi_offset) {
+ pci_read_config_dword(pci_dev,
+ msi_offset + QCOM_VFIO_MSI_ADDR_LO_OFFSET,
+ &msi_addr_lo);
+ pci_read_config_dword(pci_dev,
+ msi_offset + QCOM_VFIO_MSI_ADDR_HI_OFFSET,
+ &msi_addr_hi);
+ pci_read_config_word(pci_dev,
+ msi_offset + QCOM_VFIO_MSI_DATA_OFFSET,
+ &msi_data);
+
+ if (msi_addr_lo && msi_addr_lo != 0xFFFFFFFF) {
+ ab->pci.msi.addr_lo = msi_addr_lo;
+ ab->pci.msi.addr_hi = msi_addr_hi;
+ ab->pci.msi.ep_base_data = msi_data;
+ ath11k_info(ab, "VFIO MSI cache at offset 0x%x: addr_lo=0x%x addr_hi=0x%x data=%d\n",
+ msi_offset, msi_addr_lo, msi_addr_hi, msi_data);
+ return 0;
+ }
+ }
+
ath11k_pci_msi_disable(ab_pci);
msi_desc = irq_get_msi_desc(ab_pci->pdev->irq);
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 6/7] ath12k: add PCIe link recovery retry
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
` (4 preceding siblings ...)
2026-09-30 14:08 ` [PATCH 5/7] ath11k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 7/7] ath12k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
When ath12k devices are passed through to VMs via PCIe passthrough,
the PCIe link may not stabilize immediately after global reset due to
virtualization layer timing variations. This causes MHI initialization
to fail with "BHI offset: 0xffffffff is out of range" errors because
the device is read before it's ready.
Currently, ath12k_pci_soc_global_reset() just warns if the link is
down but continues anyway, leading to inevitable MHI failures.
This patch adds:
1. A retry loop (up to 5000ms) to wait for the PCIe link to recover
after global reset (similar to the existing LTSSM retry logic)
2. Proper error propagation to prevent continuing with a dead link
This is especially important for VM environments where timing can vary
significantly from bare metal.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/net/wireless/ath/ath12k/pci.c | 39 ++++++++++++++++++++++-----
1 file changed, 32 insertions(+), 7 deletions(-)
diff --git a/drivers/net/wireless/ath/ath12k/pci.c b/drivers/net/wireless/ath/ath12k/pci.c
index af0e882fd0b5..65712486d312 100644
--- a/drivers/net/wireless/ath/ath12k/pci.c
+++ b/drivers/net/wireless/ath/ath12k/pci.c
@@ -181,8 +181,10 @@ static void ath12k_pci_restore_window(struct ath12k_base *ab)
spin_unlock_bh(&ab_pci->window_lock);
}
-static void ath12k_pci_soc_global_reset(struct ath12k_base *ab)
+static int ath12k_pci_soc_global_reset(struct ath12k_base *ab)
{
+ unsigned long timeout;
+ bool link_recovered = false;
u32 val, delay;
val = ath12k_pci_read32(ab, PCIE_SOC_GLOBAL_RESET);
@@ -206,14 +208,29 @@ static void ath12k_pci_soc_global_reset(struct ath12k_base *ab)
mdelay(delay);
- val = ath12k_pci_read32(ab, PCIE_SOC_GLOBAL_RESET);
- if (val == 0xffffffff)
- ath12k_warn(ab, "link down error during global reset\n");
+ /* Wait for PCIe link to recover, especially important in VM environments
+ * where timing can vary significantly from bare metal.
+ */
+ timeout = jiffies + msecs_to_jiffies(5000);
+ while (time_before(jiffies, timeout)) {
+ val = ath12k_pci_read32(ab, PCIE_SOC_GLOBAL_RESET);
+ if (val != 0xffffffff) {
+ link_recovered = true;
+ break;
+ }
+ mdelay(20);
+ }
+ if (!link_recovered) {
+ ath12k_err(ab, "PCIe link failed to recover - device may need power cycle\n");
+ return -ETIMEDOUT;
+ }
/* Restore window register as its content is cleared during
* hardware global reset, such that it aligns with host cache.
*/
ath12k_pci_restore_window(ab);
+
+ return 0;
}
static void ath12k_pci_clear_dbg_registers(struct ath12k_base *ab)
@@ -299,8 +316,10 @@ static void ath12k_pci_force_wake(struct ath12k_base *ab)
mdelay(5);
}
-static void ath12k_pci_sw_reset(struct ath12k_base *ab, bool power_on)
+static int ath12k_pci_sw_reset(struct ath12k_base *ab, bool power_on)
{
+ int ret;
+
if (power_on) {
ath12k_pci_enable_ltssm(ab);
ath12k_pci_clear_all_intrs(ab);
@@ -309,8 +328,12 @@ static void ath12k_pci_sw_reset(struct ath12k_base *ab, bool power_on)
ath12k_mhi_clear_vector(ab);
ath12k_pci_clear_dbg_registers(ab);
- ath12k_pci_soc_global_reset(ab);
+ ret = ath12k_pci_soc_global_reset(ab);
+ if (ret)
+ return ret;
ath12k_mhi_set_mhictrl_reset(ab);
+
+ return 0;
}
static void ath12k_pci_free_ce_irq(struct ath12k_base *ab, int num_ce)
@@ -1452,7 +1475,9 @@ int ath12k_pci_power_up(struct ath12k_base *ab)
ab_pci->register_window = 0;
clear_bit(ATH12K_PCI_FLAG_INIT_DONE, &ab_pci->flags);
- ath12k_pci_sw_reset(ab_pci->ab, true);
+ ret = ath12k_pci_sw_reset(ab_pci->ab, true);
+ if (ret)
+ return ret;
/* Disable ASPM during firmware download due to problems switching
* to AMSS state.
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 7/7] ath12k: Use VFIO MSI cache when available
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
` (5 preceding siblings ...)
2026-09-30 14:08 ` [PATCH 6/7] ath12k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
@ 2026-09-30 14:08 ` Jose Ignacio Tornos Martinez
6 siblings, 0 replies; 9+ messages in thread
From: Jose Ignacio Tornos Martinez @ 2026-09-30 14:08 UTC (permalink / raw)
To: bhelgaas, alex, jjohnson
Cc: johannes, mani, jgg, yishaih, skolothumtho, kevin.tian,
linux-pci, kvm, linux-wireless, ath11k, ath12k, linux-arm-msm,
linux-kernel, Jose Ignacio Tornos Martinez
The Qualcomm ath12k firmware requires physical host MSI addresses for
interrupt configuration but cannot handle IOVA-based MSI addressing in
virtualized environments.
When the device is passed through to a VM via VFIO:
- The guest kernel allocates MSI vectors and sees IOVA addresses
- The ath12k firmware expects physical host MSI addresses
- Providing IOVA to the firmware causes interrupt delivery to fail
- Result: device is non-functional in VMs
The qcom-vfio-pci variant driver (introduced in earlier patches) solves
this by caching the physical host MSI values and exposing them through
PCI extended config space with a "QMSI" magic signature for discovery.
Search for the QMSI magic signature in extended config space. If found,
the device is running on a VM and we can read the cached host MSI values
from that location. If the VFIO cache is not present (i.e. on baremetal),
fall back to standard PCI MSI configuration.
This change is transparent, arch independent and requires no user
configuration when used with the qcom-vfio-pci driver.
Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
---
drivers/net/wireless/ath/ath12k/pci.c | 43 +++++++++++++++++++++++++++
drivers/net/wireless/ath/ath12k/pci.h | 6 ++++
2 files changed, 49 insertions(+)
diff --git a/drivers/net/wireless/ath/ath12k/pci.c b/drivers/net/wireless/ath/ath12k/pci.c
index 65712486d312..832d3cf652d9 100644
--- a/drivers/net/wireless/ath/ath12k/pci.c
+++ b/drivers/net/wireless/ath/ath12k/pci.c
@@ -8,6 +8,7 @@
#include <linux/interrupt.h>
#include <linux/msi.h>
#include <linux/pci.h>
+#include <linux/qcom_vfio.h>
#include <linux/time.h>
#include <linux/vmalloc.h>
@@ -783,8 +784,11 @@ static int ath12k_pci_msi_alloc(struct ath12k_pci *ab_pci)
{
struct ath12k_base *ab = ab_pci->ab;
const struct ath12k_msi_config *msi_config = ab_pci->msi_config;
+ u32 msi_addr_lo = 0, msi_addr_hi = 0;
struct msi_desc *msi_desc;
+ u16 msi_data = 0;
int num_vectors;
+ int msi_offset;
int ret;
num_vectors = pci_alloc_irq_vectors(ab_pci->pdev,
@@ -812,6 +816,39 @@ static int ath12k_pci_msi_alloc(struct ath12k_pci *ab_pci)
ath12k_info(ab, "MSI vectors: %d\n", num_vectors);
+ /*
+ * Try to get host MSI address from VFIO cache.
+ * When running on a VM, the ath12k firmware requires physical host
+ * MSI addresses and cannot handle guest IOVA addresses.
+ * qcom-vfio-pci writes host MSI values to extended config space with
+ * a magic signature "QMSI" for discovery. Search for it.
+ */
+ msi_offset = qcom_vfio_find_msi_cache(ab_pci->pdev);
+ if (msi_offset) {
+ pci_read_config_dword(ab_pci->pdev,
+ msi_offset + QCOM_VFIO_MSI_ADDR_LO_OFFSET,
+ &msi_addr_lo);
+ pci_read_config_dword(ab_pci->pdev,
+ msi_offset + QCOM_VFIO_MSI_ADDR_HI_OFFSET,
+ &msi_addr_hi);
+ pci_read_config_word(ab_pci->pdev,
+ msi_offset + QCOM_VFIO_MSI_DATA_OFFSET,
+ &msi_data);
+
+ if (msi_addr_lo && msi_addr_lo != 0xFFFFFFFF) {
+ ab_pci->msi_addr_hi = msi_addr_hi;
+ ab_pci->msi_addr_lo = msi_addr_lo;
+ ab_pci->msi_ep_base_data = msi_data;
+ ab_pci->msi_addr_cached = true;
+
+ ath12k_info(ab,
+ "using host MSI from VFIO at offset 0x%x: addr_lo=0x%x addr_hi=0x%x data=%d\n",
+ msi_offset, msi_addr_lo, msi_addr_hi, msi_data);
+ return 0;
+ }
+ }
+ ab_pci->msi_addr_cached = false;
+
ath12k_pci_msi_disable(ab_pci);
msi_desc = irq_get_msi_desc(ab_pci->pdev->irq);
@@ -1083,6 +1120,12 @@ void ath12k_pci_get_msi_address(struct ath12k_base *ab, u32 *msi_addr_lo,
struct ath12k_pci *ab_pci = ath12k_pci_priv(ab);
struct pci_dev *pci_dev = to_pci_dev(ab->dev);
+ if (ab_pci->msi_addr_cached) {
+ *msi_addr_hi = ab_pci->msi_addr_hi;
+ *msi_addr_lo = ab_pci->msi_addr_lo;
+ return;
+ }
+
pci_read_config_dword(pci_dev, pci_dev->msi_cap + PCI_MSI_ADDRESS_LO,
msi_addr_lo);
diff --git a/drivers/net/wireless/ath/ath12k/pci.h b/drivers/net/wireless/ath/ath12k/pci.h
index 0e0e2020c6ae..ee3696d40dfb 100644
--- a/drivers/net/wireless/ath/ath12k/pci.h
+++ b/drivers/net/wireless/ath/ath12k/pci.h
@@ -129,6 +129,12 @@ struct ath12k_pci {
/* enum ath12k_pci_flags */
unsigned long flags;
u16 link_ctl;
+
+ /* Cached host MSI address for firmware configuration in VMs */
+ u32 msi_addr_hi;
+ u32 msi_addr_lo;
+ bool msi_addr_cached;
+
unsigned long irq_flags;
const struct ath12k_pci_ops *pci_ops;
u32 qmi_instance;
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver
2026-09-30 14:08 ` [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver Jose Ignacio Tornos Martinez
@ 2026-09-30 15:12 ` Jason Gunthorpe
0 siblings, 0 replies; 9+ messages in thread
From: Jason Gunthorpe @ 2026-09-30 15:12 UTC (permalink / raw)
To: Jose Ignacio Tornos Martinez
Cc: bhelgaas, alex, jjohnson, johannes, mani, yishaih, skolothumtho,
kevin.tian, linux-pci, kvm, linux-wireless, ath11k, ath12k,
linux-arm-msm, linux-kernel
On Wed, Sep 30, 2026 at 04:08:29PM +0200, Jose Ignacio Tornos Martinez wrote:
> Add VFIO variant driver for Qualcomm PCIe devices that require
> MSI address passthrough for VM operation.
>
> Qualcomm ath11k and ath12k WiFi devices have embedded interrupt
> controllers that require physical host MSI addresses programmed to
> device registers. In VMs, the driver only sees virtualized guest
> addresses, causing firmware initialization to fail.
>
> This variant driver:
> 1. Caches physical host MSI values after allocation
> 2. Writes them to extended config space with magic signature "QMSI"
> 3. VM drivers discover and use these values automatically
You should probably explain a little be more here
1) Linux VM driver fills up the normal MSI-X table
2) HW has some non-MSI-X table registers
3) Linux VM driver pokes into the interrupt layer and extracts
one of the MSX-X table entries addr/data pair
4) Linux VM driver now programs that copied addr/data pair into #2
This is, of course, all wrong. These days it should be using one of
our mechanisms to allow devices to have their own private MSI
registers.
I forget if this is the right way for PCI, but the driver can call
platform_device_msi_init_and_alloc_irqs()
And directly program the MSI registers with their own special
interrupts vectors, no copying from MSI-X.
If the driver is fixed to work like this, as it should be, then it
fully breaks the scheme you propose here. That's not good.
The problem here is not really a qcom problem, and treating it as a
qcom quirk is why it keeps being stuck, IMHO.
The real issue is that this device MSI scheme does not work in VMs at
all. It does not work because the VM IRQ design requires the VM to
trap and modify all the MSI addr/data pairs at the register write.
The technically clean solution is to redo the VMMs so they don't
require that, ie use interrupt remapping so the VM's view of the
addr/data pair matches physical. That's super hard and will probably
never happen.
But! Now that we have these device MSI domains I wonder if there is
some half option to provide a hypercall so the device MSI domains can
call out to the hypervisor to get the true physical addr/data pair to
program?
This is fundamentally an irq layer issue in Linux, not a qcom one.
Jason
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-09-30 15:12 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-30 14:08 [PATCH 0/7] Enable Qualcomm WiFi PCIe passthrough to VMs Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 1/7] PCI: Add pci_find_free_ext_cap_offset() helper Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 2/7] vfio: Add qcom_vfio.h header for MSI cache protocol Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 3/7] vfio/pci: Add qcom-vfio-pci variant driver Jose Ignacio Tornos Martinez
2026-09-30 15:12 ` Jason Gunthorpe
2026-09-30 14:08 ` [PATCH 4/7] ath11k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 5/7] ath11k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 6/7] ath12k: add PCIe link recovery retry Jose Ignacio Tornos Martinez
2026-09-30 14:08 ` [PATCH 7/7] ath12k: Use VFIO MSI cache when available Jose Ignacio Tornos Martinez
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®