* [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver
@ 2026-11-03 2:51 Mukesh R
2026-11-03 2:51 ` [PATCH V3 1/5] mshv: Add gfp flags to deposit memory logic Mukesh R
` (4 more replies)
0 siblings, 5 replies; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
This short series introduces hyperv root VM iommu kernel only driver. This
driver acts as liason between the hypervisor and iommu layer. Details are
both in the actual commits and in the comments in the file implementing it.
Thanks,
-Mukesh
V0: based on upstream hyperv-next commit be0cfab740e5
V1:
- change order of lookup tree deletions to appease all AI agents,
carving out hv_iommu_hyp_unmap_pages() from hv_iommu_unmap_pages().
- change unique_id to ida subsystem management so we never ever run
out of domain ids.
- change hv_max_iova_width to hv_iommu_max_iova and generate proper
mask.
- some other minor changes.
V2:
- Add patch (0001) to allow gfp_flags to be passed to deposit pages
function.
- In last main patch:
o remove hv_iommu_setup_skip to skip devices being managed
o remove hv_special_domain()
o Add comments around pgsize_bitmap and IOMMU capabilities.
o Add to lookup tree before mapping iova in the map callback
o Unmap iova before removing from the tree in the unmap callback
V3:
- Rebase to rc1 tag 6628e7d4c6b2 which contains both pviommu and the
deposit memory redesign patches.
- Add a tiny patch to rename hv_iommu_init() to hv_pviommu_init(), and
only call it for non-root.
Mukesh R (5):
mshv: Add gfp flags to deposit memory logic
PCI: hv: Export hv_build_devid_type_pci() and change return type
mshv: Import data structs around device domains from hyperv headers
iommu/hyperv: Only call pviommu init on non-root VMs
x86/hyperv: Implement root partition IOMMU kernel only driver
arch/x86/hyperv/hv_init.c | 3 +-
arch/x86/hyperv/irqdomain.c | 9 +-
arch/x86/include/asm/mshyperv.h | 8 +-
arch/x86/kernel/pci-dma.c | 2 +
drivers/hv/hv_proc.c | 20 +-
drivers/iommu/hyperv/Kconfig | 13 +
drivers/iommu/hyperv/Makefile | 1 +
drivers/iommu/hyperv/hv-iommu-guest.c | 2 +-
drivers/iommu/hyperv/hv-iommu-root.c | 602 ++++++++++++++++++++++++++
include/asm-generic/mshyperv.h | 17 +-
include/hyperv/hvgdk_mini.h | 4 +
include/hyperv/hvhdk_mini.h | 15 +
include/linux/hyperv.h | 6 +
13 files changed, 683 insertions(+), 19 deletions(-)
create mode 100644 drivers/iommu/hyperv/hv-iommu-root.c
--
2.51.2.vfs.0.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH V3 1/5] mshv: Add gfp flags to deposit memory logic
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
@ 2026-11-03 2:51 ` Mukesh R
2026-11-03 2:51 ` [PATCH V3 2/5] PCI: hv: Export hv_build_devid_type_pci() and change return type Mukesh R
` (3 subsequent siblings)
4 siblings, 0 replies; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
Currently GFP_KERNEL is hard coded in hv_alloc_dep_pages(). Add gfp_flags
parameter so it can be called in atomic context from map_pages callback in
struct iommu_domain_ops and other places in future. A new function
hv_deposit_memory_gfp() is added to facilitate this, so hv_deposit_memory()
can continue to function as is.
Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
---
drivers/hv/hv_proc.c | 20 +++++++++++---------
include/asm-generic/mshyperv.h | 14 +++++++++++---
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/drivers/hv/hv_proc.c b/drivers/hv/hv_proc.c
index f811bb5c42a1..8380e9050f46 100644
--- a/drivers/hv/hv_proc.c
+++ b/drivers/hv/hv_proc.c
@@ -22,7 +22,7 @@
* Returns: number of pages allocated or -ENOMEM
*/
static int hv_alloc_dep_pages(int node, u64 *pfna, u64 *lastpfnp, int num_pages,
- bool single)
+ bool single, gfp_t gfp_flags)
{
struct page *page;
int num_allocd, count = 0;
@@ -37,7 +37,6 @@ static int hv_alloc_dep_pages(int node, u64 *pfna, u64 *lastpfnp, int num_pages,
while (num_pages) {
/* Find highest order we can actually allocate */
int order = 31 - __builtin_clz(num_pages);
- gfp_t gfp_flags = GFP_KERNEL;
if (!single)
gfp_flags |= __GFP_NOWARN;
@@ -81,7 +80,8 @@ static int hv_alloc_dep_pages(int node, u64 *pfna, u64 *lastpfnp, int num_pages,
* so we must split that also. Lastly, HV_MAX_CONTIGUOUS_ALLOCATION_PAGES is
* not guaranteed to always be power of 2.
*/
-static int hv_call_deposit_pages(int node, u64 partition_id, bool contiguous)
+static int hv_call_deposit_pages(int node, u64 partition_id, bool contiguous,
+ gfp_t gfp_flags)
{
struct hv_deposit_memory *hc_input;
int i, rc, num_pages;
@@ -98,14 +98,15 @@ static int hv_call_deposit_pages(int node, u64 partition_id, bool contiguous)
num_pages = HV_DEPOSIT_MAX;
}
- hc_input = (struct hv_deposit_memory *)get_zeroed_page(GFP_KERNEL);
+ hc_input = (struct hv_deposit_memory *)get_zeroed_page(gfp_flags);
if (hc_input == NULL)
return -ENOMEM;
hc_input->partition_id = partition_id;
pfna = hc_input->gpa_page_list;
- rc = hv_alloc_dep_pages(node, pfna, &lastpfn, num_pages, contiguous);
+ rc = hv_alloc_dep_pages(node, pfna, &lastpfn, num_pages, contiguous,
+ gfp_flags);
if (rc < 0)
goto out_free;
@@ -159,7 +160,7 @@ static int hv_call_deposit_pages(int node, u64 partition_id, bool contiguous)
return rc;
}
-int hv_deposit_memory_node(int node, u64 pt_id, u64 hv_status)
+int hv_deposit_memory_node(int node, u64 pt_id, u64 hv_status, gfp_t gfp_flags)
{
int result = hv_result(hv_status);
bool contiguous = false;
@@ -179,7 +180,7 @@ int hv_deposit_memory_node(int node, u64 pt_id, u64 hv_status)
result == HV_STATUS_INSUFFICIENT_CONTIGUOUS_ROOT_MEMORY)
contiguous = true;
- return hv_call_deposit_pages(node, pt_id, contiguous);
+ return hv_call_deposit_pages(node, pt_id, contiguous, gfp_flags);
}
EXPORT_SYMBOL_GPL(hv_deposit_memory_node);
@@ -232,7 +233,7 @@ int hv_call_add_logical_proc(int node, u32 lp_index, u32 apic_id)
break;
}
ret = hv_deposit_memory_node(node, hv_current_partition_id,
- status);
+ status, GFP_KERNEL);
} while (!ret);
return ret;
@@ -266,7 +267,8 @@ int hv_call_create_vp(int node, u64 partition_id, u32 vp_index, u32 flags)
}
break;
}
- ret = hv_deposit_memory_node(node, partition_id, status);
+ ret = hv_deposit_memory_node(node, partition_id, status,
+ GFP_KERNEL);
} while (!ret);
diff --git a/include/asm-generic/mshyperv.h b/include/asm-generic/mshyperv.h
index 4c9f3353074a..2584e693d7b3 100644
--- a/include/asm-generic/mshyperv.h
+++ b/include/asm-generic/mshyperv.h
@@ -346,7 +346,7 @@ static inline bool hv_parent_partition(void)
}
bool hv_result_needs_memory(u64 status);
-int hv_deposit_memory_node(int node, u64 partition_id, u64 status);
+int hv_deposit_memory_node(int node, u64 partition_id, u64 status, gfp_t gfp);
int hv_call_add_logical_proc(int node, u32 lp_index, u32 acpi_id);
int hv_call_notify_all_processors_started(void);
bool hv_lp_exists(u32 lp_index);
@@ -357,7 +357,8 @@ static inline bool hv_root_partition(void) { return false; }
static inline bool hv_l1vh_partition(void) { return false; }
static inline bool hv_parent_partition(void) { return false; }
static inline bool hv_result_needs_memory(u64 status) { return false; }
-static inline int hv_deposit_memory_node(int node, u64 partition_id, u64 status)
+static inline int hv_deposit_memory_node(int node, u64 partition_id, u64 status,
+ gfp_t gfp)
{
return -EOPNOTSUPP;
}
@@ -379,9 +380,16 @@ static inline int hv_call_create_vp(int node, u64 partition_id, u32 vp_index, u3
}
#endif /* CONFIG_MSHV_ROOT */
+/* Generic deposit function with no default numa node and GFP_KERNEL */
static inline int hv_deposit_memory(u64 partition_id, u64 status)
{
- return hv_deposit_memory_node(NUMA_NO_NODE, partition_id, status);
+ return hv_deposit_memory_node(NUMA_NO_NODE, partition_id, status,
+ GFP_KERNEL);
+}
+
+static inline int hv_deposit_memory_gfp(u64 partition_id, u64 status, gfp_t gfp)
+{
+ return hv_deposit_memory_node(NUMA_NO_NODE, partition_id, status, gfp);
}
#if IS_ENABLED(CONFIG_HYPERV_VTL_MODE)
--
2.51.2.vfs.0.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH V3 2/5] PCI: hv: Export hv_build_devid_type_pci() and change return type
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
2026-11-03 2:51 ` [PATCH V3 1/5] mshv: Add gfp flags to deposit memory logic Mukesh R
@ 2026-11-03 2:51 ` Mukesh R
2026-11-03 2:51 ` [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers Mukesh R
` (2 subsequent siblings)
4 siblings, 0 replies; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
On Hyper-V, most hypercalls related to device domain management like
attach device to a device domain, or to map/unmap regions, interrupts,
etc need a device ID as a parameter. So, make hv_build_devid_type_pci()
public and change return type to u64 to enforce its size.
Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
Reviewed-by: Souradeep Chakrabarti <schakrabarti@linux.microsoft.com>
---
arch/x86/hyperv/irqdomain.c | 9 +++++----
arch/x86/include/asm/mshyperv.h | 6 ++++++
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/arch/x86/hyperv/irqdomain.c b/arch/x86/hyperv/irqdomain.c
index b3ad50a874dc..8780573a4332 100644
--- a/arch/x86/hyperv/irqdomain.c
+++ b/arch/x86/hyperv/irqdomain.c
@@ -112,7 +112,7 @@ static int get_rid_cb(struct pci_dev *pdev, u16 alias, void *data)
return 0;
}
-static union hv_device_id hv_build_devid_type_pci(struct pci_dev *pdev)
+u64 hv_build_devid_type_pci(struct pci_dev *pdev)
{
int pos;
union hv_device_id hv_devid;
@@ -172,8 +172,9 @@ static union hv_device_id hv_build_devid_type_pci(struct pci_dev *pdev)
}
out:
- return hv_devid;
+ return hv_devid.as_uint64;
}
+EXPORT_SYMBOL_GPL(hv_build_devid_type_pci);
/*
* hv_map_msi_interrupt() - Map the MSI IRQ in the hypervisor.
@@ -196,7 +197,7 @@ int hv_map_msi_interrupt(struct irq_data *data,
msidesc = irq_data_get_msi_desc(data);
pdev = msi_desc_to_pci_dev(msidesc);
- hv_devid = hv_build_devid_type_pci(pdev);
+ hv_devid.as_uint64 = hv_build_devid_type_pci(pdev);
cpu = cpumask_first(irq_data_get_effective_affinity_mask(data));
return hv_map_interrupt(hv_devid, false, cpu, cfg->vector,
@@ -271,7 +272,7 @@ static int hv_unmap_msi_interrupt(struct pci_dev *pdev,
{
union hv_device_id hv_devid;
- hv_devid = hv_build_devid_type_pci(pdev);
+ hv_devid.as_uint64 = hv_build_devid_type_pci(pdev);
return hv_unmap_interrupt(hv_devid.as_uint64, irq_entry);
}
diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h
index 20d947c2c758..24b19cd28a05 100644
--- a/arch/x86/include/asm/mshyperv.h
+++ b/arch/x86/include/asm/mshyperv.h
@@ -248,6 +248,12 @@ void hv_crash_asm_end(void);
static inline void hv_root_crash_init(void) {}
#endif /* CONFIG_MSHV_ROOT && CONFIG_CRASH_DUMP */
+#ifdef CONFIG_PCI_MSI
+u64 hv_build_devid_type_pci(struct pci_dev *pdev);
+#else
+static inline u64 hv_build_devid_type_pci(struct pci_dev *pdev) { return 0; }
+#endif
+
#else /* CONFIG_HYPERV */
static inline void hyperv_init(void) {}
static inline void hyperv_setup_mmu_ops(void) {}
--
2.51.2.vfs.0.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
2026-11-03 2:51 ` [PATCH V3 1/5] mshv: Add gfp flags to deposit memory logic Mukesh R
2026-11-03 2:51 ` [PATCH V3 2/5] PCI: hv: Export hv_build_devid_type_pci() and change return type Mukesh R
@ 2026-11-03 2:51 ` Mukesh R
2026-11-03 3:17 ` Easwar Hariharan
2026-11-03 2:51 ` [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs Mukesh R
2026-11-03 2:51 ` [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver Mukesh R
4 siblings, 1 reply; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
Copy/import from Hyper-V public headers, definitions and hypercalls that
are related to checking for device domain availibility, and mapping and
unmapping of device pages.
Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
Reviewed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
---
include/hyperv/hvgdk_mini.h | 4 ++++
include/hyperv/hvhdk_mini.h | 15 +++++++++++++++
2 files changed, 19 insertions(+)
diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
index 966945f0504f..606b16f43e16 100644
--- a/include/hyperv/hvgdk_mini.h
+++ b/include/hyperv/hvgdk_mini.h
@@ -327,6 +327,8 @@ union hv_hypervisor_version_info {
/* stimer Direct Mode is available */
#define HV_STIMER_DIRECT_MODE_AVAILABLE BIT(19)
+#define HV_DEVICE_DOMAIN_AVAILABLE BIT(24)
+
/*
* Implementation recommendations. Indicates which behaviors the hypervisor
* recommends the OS implement for optimal performance.
@@ -489,6 +491,8 @@ union hv_vp_assist_msr_contents { /* HV_REGISTER_VP_ASSIST_PAGE */
#define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_LIST 0x00b0
#define HVCALL_CREATE_DEVICE_DOMAIN 0x00b1
#define HVCALL_ATTACH_DEVICE_DOMAIN 0x00b2
+#define HVCALL_MAP_DEVICE_GPA_PAGES 0x00b3
+#define HVCALL_UNMAP_DEVICE_GPA_PAGES 0x00b4
#define HVCALL_SIGNAL_EVENT_DIRECT 0x00c0
#define HVCALL_POST_MESSAGE_DIRECT 0x00c1
#define HVCALL_DISPATCH_VP 0x00c2
diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
index 57b70efe60d4..2bb757e00d86 100644
--- a/include/hyperv/hvhdk_mini.h
+++ b/include/hyperv/hvhdk_mini.h
@@ -549,6 +549,7 @@ union hv_device_id { /* HV_DEVICE_ID */
} __packed;
/* Device domain types */
+#define HV_DEVICE_DOMAIN_TYPE_S2 0 /* HV_DEVICE_DOMAIN_ID_TYPE_S2 */
#define HV_DEVICE_DOMAIN_TYPE_S1 1 /* HV_DEVICE_DOMAIN_ID_TYPE_S1 */
/* ID for default domain and NULL domain */
@@ -657,6 +658,20 @@ struct hv_output_get_iommu_capabilities {
u64 pgsize_bitmap;
} __packed;
+struct hv_input_map_device_gpa_pages { /* HV_INPUT_MAP_DEVICE_GPA_PAGES */
+struct hv_input_device_domain device_domain;
+ union hv_input_vtl target_vtl;
+ u8 padding[3];
+ u32 map_flags;
+ u64 target_device_va_base;
+ u64 gpa_page_list[];
+} __packed;
+
+struct hv_input_unmap_device_gpa_pages { /* HV_INPUT_UNMAP_DEVICE_GPA_PAGES */
+ struct hv_input_device_domain device_domain;
+ u64 target_device_va_base;
+} __packed;
+
/* HV_LOGICAL_DEVICE_PROPERTY_CODE */
enum hv_logical_device_property_code {
HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU = 10,
--
2.51.2.vfs.0.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
` (2 preceding siblings ...)
2026-11-03 2:51 ` [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers Mukesh R
@ 2026-11-03 2:51 ` Mukesh R
2026-11-03 2:57 ` sashiko-bot
2026-11-03 2:51 ` [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver Mukesh R
4 siblings, 1 reply; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
Next patch introduces root iommu init. Since both can co-exist, set
x86 iommu_init to pviommu init only on non-root VMs. Also, rename
hv_iommu_init() to hv_pviommu_init(). Lastly, check for null iommu_init
before assignement as suggested by one of the pviommu authors.
Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
---
arch/x86/hyperv/hv_init.c | 3 ++-
arch/x86/include/asm/mshyperv.h | 2 +-
drivers/iommu/hyperv/hv-iommu-guest.c | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/arch/x86/hyperv/hv_init.c b/arch/x86/hyperv/hv_init.c
index b90e6f487533..18651c38dba3 100644
--- a/arch/x86/hyperv/hv_init.c
+++ b/arch/x86/hyperv/hv_init.c
@@ -580,7 +580,8 @@ void __init hyperv_init(void)
x86_init.timers.setup_percpu_clockev = hv_stimer_setup_percpu_clockev;
#ifdef CONFIG_HYPERV_PVIOMMU
- x86_init.iommu.iommu_init = hv_iommu_init;
+ if (!hv_root_partition() && x86_init.iommu.iommu_init == NULL)
+ x86_init.iommu.iommu_init = hv_pviommu_init;
#endif
hv_apic_init();
diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h
index 24b19cd28a05..c95d75ae645e 100644
--- a/arch/x86/include/asm/mshyperv.h
+++ b/arch/x86/include/asm/mshyperv.h
@@ -320,7 +320,7 @@ static inline void __mshv_vtl_return_call(struct mshv_vtl_cpu_context *vtl0) {}
#endif
#ifdef CONFIG_HYPERV_PVIOMMU
-int __init hv_iommu_init(void);
+int __init hv_pviommu_init(void);
#endif
#include <asm-generic/mshyperv.h>
diff --git a/drivers/iommu/hyperv/hv-iommu-guest.c b/drivers/iommu/hyperv/hv-iommu-guest.c
index 876a22f21f1e..a40a8ceb3261 100644
--- a/drivers/iommu/hyperv/hv-iommu-guest.c
+++ b/drivers/iommu/hyperv/hv-iommu-guest.c
@@ -601,7 +601,7 @@ hv_init_iommu_device(struct hv_iommu_dev *hv_iommu,
hv_iommu_device = hv_iommu;
}
-int __init hv_iommu_init(void)
+int __init hv_pviommu_init(void)
{
u64 status;
int ret = 0;
--
2.51.2.vfs.0.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
` (3 preceding siblings ...)
2026-11-03 2:51 ` [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs Mukesh R
@ 2026-11-03 2:51 ` Mukesh R
2026-11-03 3:25 ` sashiko-bot
4 siblings, 1 reply; 9+ messages in thread
From: Mukesh R @ 2026-11-03 2:51 UTC (permalink / raw)
To: linux-hyperv, linux-kernel, iommu, linux-arch
Cc: jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd, mrathor
Add a new file to implement a kernel only virtual IOMMU that works
on Microsoft Hyper-V hypervisor (aka MSHV) in privileged VMs aka
root VMs. The hypervisor claims the IOMMU upon boot, and this driver
communicates with it for creating and deleting paging domains, attaching
of devices, mapping and unmapping of pages, etc. During boot, hypervisor
automatically creates identity and blocked domains, so there is no need
to do hypercalls to create them. This is a kernel only driver and only
supported on baremetal root (and not L1VH root) without any guest passthru
support. Support for guest device passthru will be added incrementally.
Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
---
arch/x86/kernel/pci-dma.c | 2 +
drivers/iommu/hyperv/Kconfig | 13 +
drivers/iommu/hyperv/Makefile | 1 +
drivers/iommu/hyperv/hv-iommu-root.c | 602 +++++++++++++++++++++++++++
include/asm-generic/mshyperv.h | 3 +
include/linux/hyperv.h | 6 +
6 files changed, 627 insertions(+)
create mode 100644 drivers/iommu/hyperv/hv-iommu-root.c
diff --git a/arch/x86/kernel/pci-dma.c b/arch/x86/kernel/pci-dma.c
index 75cf8f6ae8cd..b383c4549615 100644
--- a/arch/x86/kernel/pci-dma.c
+++ b/arch/x86/kernel/pci-dma.c
@@ -8,6 +8,7 @@
#include <linux/gfp.h>
#include <linux/pci.h>
#include <linux/amd-iommu.h>
+#include <linux/hyperv.h>
#include <asm/proto.h>
#include <asm/dma.h>
@@ -101,6 +102,7 @@ void __init pci_iommu_alloc(void)
}
pci_swiotlb_detect();
gart_iommu_hole_init();
+ hv_root_iommu_detect();
amd_iommu_detect();
detect_intel_iommu();
swiotlb_init(x86_swiotlb_enable, x86_swiotlb_flags);
diff --git a/drivers/iommu/hyperv/Kconfig b/drivers/iommu/hyperv/Kconfig
index f2f993f647c3..6ba5125ed501 100644
--- a/drivers/iommu/hyperv/Kconfig
+++ b/drivers/iommu/hyperv/Kconfig
@@ -13,3 +13,16 @@ config HYPERV_PVIOMMU
Microsoft Hyper-V. Provides DMA remapping and IOTLB
flush support to enable DMA isolation for PCI devices
assigned to the guest.
+
+config HYPERV_ROOT_IOMMU
+ bool "Hyper-V IOMMU Device in root partition"
+ depends on HYPERV && X86 && PCI_MSI && MSHV_ROOT
+ select IOMMU_API
+ default HYPERV
+ help
+ This enables Hyper-V pseudo IOMMU device. When running as privileged
+ VM aka root on Microsoft Hyper-V hypervisor, this must be enabled
+ for doing any PCI passthru of devices to guest VMs. This applies to
+ both PFs and VFs. When enabling this, it is best to disable amd/intel
+ iommus via: intel_iommu=off amd_iommu=off as the hypervisor really
+ owns the iommu.
diff --git a/drivers/iommu/hyperv/Makefile b/drivers/iommu/hyperv/Makefile
index d3b2a259439c..04c66b4409a3 100644
--- a/drivers/iommu/hyperv/Makefile
+++ b/drivers/iommu/hyperv/Makefile
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_IRQ_REMAP) += hv-irq-remap-x86.o
obj-$(CONFIG_HYPERV_PVIOMMU) += hv-iommu-guest.o
+obj-$(CONFIG_HYPERV_ROOT_IOMMU) += hv-iommu-root.o
diff --git a/drivers/iommu/hyperv/hv-iommu-root.c b/drivers/iommu/hyperv/hv-iommu-root.c
new file mode 100644
index 000000000000..23212c9e44e4
--- /dev/null
+++ b/drivers/iommu/hyperv/hv-iommu-root.c
@@ -0,0 +1,602 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Hyper-V root vIOMMU driver.
+ * Copyright (C) 2026, Microsoft, Inc.
+ */
+#include <linux/pci.h>
+#include <linux/dma-map-ops.h>
+#include <linux/interval_tree.h>
+#include <linux/hyperv.h>
+#include <asm/iommu.h>
+#include <asm/mshyperv.h>
+#include "../dma-iommu.h"
+
+static dma_addr_t hv_iommu_max_iova;
+static struct iommu_domain_ops hv_paging_domain_ops;
+
+/* IOMMU device that we export to the world. MSHV supports max of one */
+static struct iommu_device hv_virt_iommu;
+
+struct hv_iommu_domain {
+ struct iommu_domain iommu_dom;
+ u32 domid_num; /* as opposed to domain_id.type */
+ spinlock_t mappings_lock; /* protects mappings_tree */
+ struct rb_root_cached mappings_tree; /* iova to pa lookup tree */
+};
+
+#define to_hv_domain(d) container_of(d, struct hv_iommu_domain, iommu_dom)
+
+struct hv_iommu_mapping {
+ phys_addr_t paddr;
+ struct interval_tree_node iova;
+ u32 flags;
+};
+
+/*
+ * By default, during boot the hypervisor creates one Stage 2 (S2) default
+ * domain. It has two types:
+ * S2 default: access to entire root partition memory. This for the host
+ * maps to IOMMU_DOMAIN_IDENTITY in the iommu subsystem, and
+ * is called HV_DEVICE_DOMAIN_ID_S2_DEFAULT in the hypervisor.
+ * (Linux renamed to : HV_DEVICE_DOMAIN_ID_DEFAULT)
+ * S2 NULL: Blocks everything, ie, IOMMU_DOMAIN_BLOCKED in linux.
+ */
+
+/*
+ * Create dummy domains to correspond to hypervisor prebuilt default identity
+ * and null domains (dummy because we do not make hypercalls to create them).
+ */
+static struct hv_iommu_domain hv_def_identity_dom;
+static struct hv_iommu_domain hv_def_blocked_dom;
+
+/*
+ * The hypervisor owns the IOMMU. As such it will choose the best page size
+ * and whenever possible use larger page sizes automatically. The map gpa
+ * hypercall to map the iova however currently uses only 4K pfns as input.
+ * So we specify 4K as the IOMMU PAGE_SIZE.
+ */
+#define HV_IOMMU_PGSIZES SZ_4K
+
+static DEFINE_IDA(hv_iommu_domids); /* unique numeric ids for new domains */
+
+static bool hv_iommu_capable(struct device *dev, enum iommu_cap cap)
+{
+ switch (cap) {
+ case IOMMU_CAP_CACHE_COHERENCY:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int hv_iommu_add_tree_mapping(struct hv_iommu_domain *hvdom, ulong iova,
+ phys_addr_t paddr, size_t size, u32 flags)
+{
+ ulong irqflags;
+ struct hv_iommu_mapping *mapping;
+
+ mapping = kzalloc_obj(struct hv_iommu_mapping, GFP_ATOMIC);
+ if (!mapping)
+ return -ENOMEM;
+
+ mapping->paddr = paddr;
+ mapping->iova.start = iova;
+ mapping->iova.last = iova + size - 1;
+ mapping->flags = flags;
+
+ spin_lock_irqsave(&hvdom->mappings_lock, irqflags);
+ interval_tree_insert(&mapping->iova, &hvdom->mappings_tree);
+ spin_unlock_irqrestore(&hvdom->mappings_lock, irqflags);
+
+ return 0;
+}
+
+/* If size == 0, then last = ULONG_MAX. With iova 0, will remove everything */
+static size_t hv_iommu_del_tree_mappings(struct hv_iommu_domain *hvdom,
+ ulong iova, size_t size)
+{
+ ulong flags;
+ size_t unmapped = 0;
+ ulong last = iova + size - 1;
+ struct hv_iommu_mapping *mapping = NULL;
+ struct interval_tree_node *node, *next;
+
+ spin_lock_irqsave(&hvdom->mappings_lock, flags);
+ next = interval_tree_iter_first(&hvdom->mappings_tree, iova, last);
+ while (next) {
+ node = next;
+ mapping = container_of(node, struct hv_iommu_mapping, iova);
+ next = interval_tree_iter_next(node, iova, last);
+
+ /* Splitting of a mapping is not supported at present */
+ if (mapping->iova.start < iova)
+ break;
+
+ unmapped += mapping->iova.last - mapping->iova.start + 1;
+
+ interval_tree_remove(node, &hvdom->mappings_tree);
+ kfree(mapping);
+ }
+ spin_unlock_irqrestore(&hvdom->mappings_lock, flags);
+
+ return unmapped;
+}
+
+/* Create a new device domain in the hypervisor */
+static int hv_iommu_create_hyp_devdom(struct hv_iommu_domain *hvdom)
+{
+ u64 status;
+ struct hv_input_device_domain *ddp;
+ struct hv_input_create_device_domain *input;
+ ulong flags;
+
+ local_irq_save(flags);
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(input, 0, sizeof(*input));
+
+ ddp = &input->device_domain;
+ ddp->partition_id = HV_PARTITION_ID_SELF;
+ ddp->domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
+ ddp->domain_id.id = hvdom->domid_num;
+
+ input->create_device_domain_flags.forward_progress_required = 1;
+ input->create_device_domain_flags.inherit_owning_vtl = 0;
+
+ status = hv_do_hypercall(HVCALL_CREATE_DEVICE_DOMAIN, input, NULL);
+
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "\n");
+
+ return hv_result_to_errno(status);
+}
+
+static struct iommu_domain *hv_iommu_domain_alloc_paging(struct device *dev)
+{
+ struct hv_iommu_domain *hvdom;
+ int rc;
+
+ if (!dev_is_pci(dev))
+ return NULL;
+
+ hvdom = kzalloc_obj(struct hv_iommu_domain);
+ if (hvdom == NULL)
+ return ERR_PTR(-ENOMEM);
+
+ spin_lock_init(&hvdom->mappings_lock);
+ hvdom->mappings_tree = RB_ROOT_CACHED;
+
+ rc = ida_alloc_range(&hv_iommu_domids,
+ HV_DEVICE_DOMAIN_ID_DEFAULT + 1,
+ HV_DEVICE_DOMAIN_ID_NULL - 1, GFP_KERNEL);
+ if (rc < 0)
+ goto out_err;
+
+ hvdom->domid_num = rc;
+ hvdom->iommu_dom.pgsize_bitmap = HV_IOMMU_PGSIZES;
+ hvdom->iommu_dom.geometry.aperture_start = 0;
+ hvdom->iommu_dom.geometry.aperture_end = hv_iommu_max_iova;
+ hvdom->iommu_dom.geometry.force_aperture = true;
+ hvdom->iommu_dom.ops = &hv_paging_domain_ops;
+
+ rc = hv_iommu_create_hyp_devdom(hvdom);
+ if (rc)
+ goto out_err1;
+
+ return &hvdom->iommu_dom;
+
+out_err1:
+ ida_free(&hv_iommu_domids, hvdom->domid_num);
+out_err:
+ kfree(hvdom);
+ return ERR_PTR(rc);
+}
+
+static void hv_iommu_domain_free(struct iommu_domain *immdom)
+{
+ ulong flags;
+ u64 status;
+ struct hv_input_delete_device_domain *input;
+ struct hv_input_device_domain *ddp;
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+
+ /* Cleanup any remaining. 0 for size results in ULONG_MAX as the last */
+ hv_iommu_del_tree_mappings(hvdom, 0, 0);
+
+ local_irq_save(flags);
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ ddp = &input->device_domain;
+ memset(input, 0, sizeof(*input));
+
+ ddp->partition_id = HV_PARTITION_ID_SELF;
+ ddp->domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
+ ddp->domain_id.id = hvdom->domid_num;
+
+ status = hv_do_hypercall(HVCALL_DELETE_DEVICE_DOMAIN, input,
+ NULL);
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "\n");
+
+ ida_free(&hv_iommu_domids, hvdom->domid_num);
+ kfree(hvdom);
+}
+
+/*
+ * Attach a device to the default domain, or the null domain, or to a domain
+ * previously created in the hypervisor.
+ */
+static int hv_iommu_att_dev2dom(struct hv_iommu_domain *hvdom,
+ struct pci_dev *pdev)
+{
+ ulong flags;
+ u64 status;
+ struct hv_input_attach_device_domain *input;
+
+ local_irq_save(flags);
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(input, 0, sizeof(*input));
+
+ /* For null domain, hvdom->domid_num == HV_DEVICE_DOMAIN_ID_NULL */
+ input->device_domain.partition_id = HV_PARTITION_ID_SELF;
+ input->device_domain.domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
+ input->device_domain.domain_id.id = hvdom->domid_num;
+
+ input->device_id.as_uint64 = hv_build_devid_type_pci(pdev);
+
+ status = hv_do_hypercall(HVCALL_ATTACH_DEVICE_DOMAIN, input, NULL);
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "\n");
+
+ return hv_result_to_errno(status);
+}
+
+static int hv_iommu_attach_dev(struct iommu_domain *immdom, struct device *dev,
+ struct iommu_domain *old)
+{
+ struct pci_dev *pdev;
+ int rc;
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+
+ pdev = to_pci_dev(dev);
+
+ rc = hv_iommu_att_dev2dom(hvdom, pdev);
+ if (rc)
+ WARN(1, "Failed to attach pdev:%s\n", pci_name(pdev));
+
+ return rc;
+}
+
+static u64 hv_iommu_unmap_batch(u32 domid_num, ulong iova, u16 count)
+{
+ ulong flags;
+ struct hv_input_unmap_device_gpa_pages *input;
+ u64 status;
+
+ local_irq_save(flags);
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(input, 0, sizeof(*input));
+
+ input->device_domain.partition_id = HV_PARTITION_ID_SELF;
+ input->device_domain.domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
+ input->device_domain.domain_id.id = domid_num;
+ input->target_device_va_base = iova;
+
+ status = hv_do_rep_hypercall(HVCALL_UNMAP_DEVICE_GPA_PAGES, count,
+ 0, input, NULL);
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "iova:0x%lx count:0x%x\n", iova, count);
+
+ return status;
+}
+
+static size_t hv_iommu_hyp_unmap_pages(struct iommu_domain *immdom, ulong iova,
+ size_t size)
+{
+ u64 status;
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+ size_t tot_done = 0;
+ ulong npages = size >> HV_HYP_PAGE_SHIFT;
+
+ while (npages) {
+ int done, count = min(npages, HV_REP_COUNT_MAX);
+
+ status = hv_iommu_unmap_batch(hvdom->domid_num, iova, count);
+
+ done = hv_repcomp(status);
+ tot_done += done;
+ npages -= done;
+ iova += done << HV_HYP_PAGE_SHIFT;
+
+ if (!hv_result_success(status))
+ break;
+ }
+
+ return tot_done << HV_HYP_PAGE_SHIFT;
+}
+
+static size_t hv_iommu_unmap_pages(struct iommu_domain *immdom, ulong iova,
+ size_t pgsize, size_t pgcount,
+ struct iommu_iotlb_gather *gather)
+{
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+ size_t unmap_sz, size = pgsize * pgcount;
+
+ /* For valid input, the hypervisor guarantees unmap will succeed */
+ unmap_sz = hv_iommu_hyp_unmap_pages(immdom, iova, size);
+ if (unmap_sz != size)
+ WARN(1, "Failed to unmap device gpa(%lx/%lx)\n", unmap_sz,
+ size);
+
+ size = hv_iommu_del_tree_mappings(hvdom, iova, unmap_sz);
+ if (size != unmap_sz)
+ pr_err("%s: could not delete tree mappings (%lx:%lx/%lx)\n",
+ __func__, iova, unmap_sz, size);
+
+ return size;
+}
+
+/* Return: must return exact status from the hypercall without changes */
+static u64 hv_iommu_map_pgs(struct hv_iommu_domain *hvdom, ulong iova,
+ phys_addr_t paddr, ulong npages, u32 map_flags)
+{
+ u64 status;
+ int i;
+ struct hv_input_map_device_gpa_pages *input;
+ unsigned long flags, pfn;
+
+ local_irq_save(flags);
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(input, 0, sizeof(*input));
+
+ input->device_domain.partition_id = HV_PARTITION_ID_SELF;
+ input->device_domain.domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
+ input->device_domain.domain_id.id = hvdom->domid_num;
+ input->map_flags = map_flags;
+ input->target_device_va_base = iova;
+
+ pfn = paddr >> HV_HYP_PAGE_SHIFT;
+ for (i = 0; i < npages; i++, pfn++)
+ input->gpa_page_list[i] = pfn;
+
+ status = hv_do_rep_hypercall(HVCALL_MAP_DEVICE_GPA_PAGES, npages, 0,
+ input, NULL);
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "npgs:%lx paddr:%llx iova:%lx mf:0x%x\n",
+ npages, paddr, iova, map_flags);
+ return status;
+}
+
+#define HV_MAP_DEVICE_GPA_BATCH_SIZE \
+ ((HV_HYP_PAGE_SIZE - sizeof(struct hv_input_map_device_gpa_pages)) \
+ / sizeof(u64))
+
+/* NB: cond_resched() is in vfio_iommu_map() */
+static int hv_iommu_map_pages(struct iommu_domain *immdom, ulong iova,
+ phys_addr_t paddr, size_t pgsize, size_t pgcount,
+ int prot, gfp_t gfp, size_t *mapped)
+{
+ u32 map_flags;
+ int ret;
+ u64 status = 0;
+ ulong npages, sav_iova = iova, done = 0;
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+ size_t size = pgsize * pgcount;
+
+ map_flags = HV_MAP_GPA_READABLE; /* required */
+ map_flags |= prot & IOMMU_WRITE ? HV_MAP_GPA_WRITABLE : 0;
+
+ ret = hv_iommu_add_tree_mapping(hvdom, iova, paddr, size, map_flags);
+ if (ret)
+ return ret;
+
+ npages = size >> HV_HYP_PAGE_SHIFT;
+ while (done < npages) {
+ ulong completed, remain = npages - done;
+
+ remain = min(remain, HV_MAP_DEVICE_GPA_BATCH_SIZE);
+
+ status = hv_iommu_map_pgs(hvdom, iova, paddr, remain,
+ map_flags);
+
+ completed = hv_repcomp(status);
+ done = done + completed;
+ iova = iova + (completed << HV_HYP_PAGE_SHIFT);
+ paddr = paddr + (completed << HV_HYP_PAGE_SHIFT);
+
+ if (hv_result_needs_memory(status)) {
+ ret = hv_deposit_memory_gfp(hv_current_partition_id,
+ status, gfp);
+ if (ret)
+ break;
+ continue;
+ }
+ if (!hv_result_success(status))
+ break;
+ }
+
+ if (hv_result_success(status)) {
+ if (mapped)
+ *mapped = size;
+ } else {
+ size_t done_size = done << HV_HYP_PAGE_SHIFT;
+
+ hv_status_err(status, "pgs:%lx/%lx iova:%lx\n", done, npages,
+ sav_iova);
+
+ hv_iommu_del_tree_mappings(hvdom, sav_iova, size);
+
+ /* Hypervisor guarantees unmap will succeed here */
+ hv_iommu_hyp_unmap_pages(immdom, sav_iova, done_size);
+
+ if (mapped)
+ *mapped = 0;
+ }
+
+ return hv_result_to_errno(status);
+}
+
+static phys_addr_t hv_iommu_iova_to_phys(struct iommu_domain *immdom,
+ dma_addr_t iova)
+{
+ unsigned long flags;
+ struct hv_iommu_mapping *mapping;
+ struct interval_tree_node *node;
+ u64 paddr = 0;
+ struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
+
+ spin_lock_irqsave(&hvdom->mappings_lock, flags);
+ node = interval_tree_iter_first(&hvdom->mappings_tree, iova, iova);
+ if (node) {
+ mapping = container_of(node, struct hv_iommu_mapping, iova);
+ paddr = mapping->paddr + (iova - mapping->iova.start);
+ }
+ spin_unlock_irqrestore(&hvdom->mappings_lock, flags);
+
+ return paddr;
+}
+
+static struct iommu_device *hv_iommu_probe_device(struct device *dev)
+{
+ if (!dev_is_pci(dev))
+ return ERR_PTR(-ENODEV);
+
+ return &hv_virt_iommu;
+}
+
+static void hv_iommu_get_resv_regions(struct device *dev,
+ struct list_head *head)
+{
+ iommu_add_resv_x86_msi_region(head);
+}
+
+static struct iommu_domain_ops hv_paging_domain_ops = {
+ .attach_dev = hv_iommu_attach_dev,
+ .map_pages = hv_iommu_map_pages,
+ .unmap_pages = hv_iommu_unmap_pages,
+ .iova_to_phys = hv_iommu_iova_to_phys,
+ .free = hv_iommu_domain_free,
+};
+
+static struct iommu_ops hv_iommu_ops = {
+ .capable = hv_iommu_capable,
+ .domain_alloc_paging = hv_iommu_domain_alloc_paging,
+ .probe_device = hv_iommu_probe_device,
+ .device_group = pci_device_group,
+ .get_resv_regions = hv_iommu_get_resv_regions,
+ .owner = THIS_MODULE,
+ .identity_domain = &hv_def_identity_dom.iommu_dom,
+ .blocked_domain = &hv_def_blocked_dom.iommu_dom,
+ .release_domain = &hv_def_blocked_dom.iommu_dom,
+};
+
+static const struct iommu_domain_ops hv_special_domain_ops = {
+ .attach_dev = hv_iommu_attach_dev,
+};
+
+static void __init hv_initialize_special_domains(void)
+{
+ hv_def_identity_dom.iommu_dom.type = IOMMU_DOMAIN_IDENTITY;
+ hv_def_identity_dom.iommu_dom.ops = &hv_special_domain_ops;
+ hv_def_identity_dom.iommu_dom.owner = &hv_iommu_ops;
+ hv_def_identity_dom.domid_num = HV_DEVICE_DOMAIN_ID_DEFAULT; /* 0 */
+
+ hv_def_blocked_dom.iommu_dom.type = IOMMU_DOMAIN_BLOCKED;
+ hv_def_blocked_dom.iommu_dom.ops = &hv_special_domain_ops;
+ hv_def_blocked_dom.iommu_dom.owner = &hv_iommu_ops;
+ hv_def_blocked_dom.domid_num = HV_DEVICE_DOMAIN_ID_NULL; /* INTMAX */
+}
+
+/*
+ * In general, the hypervisor does not support the capabilities hypercall for
+ * root partitions, an exception was made for max_iova_width. For IOMMU cap,
+ * hv_root_iommu_detect() below checks for HV_DEVICE_DOMAIN_AVAILABLE which
+ * supersedes HV_IOMMU_CAP_PRESENT. As for the pgsize_bitmap cap, see comment
+ * above HV_IOMMU_PGSIZES.
+ */
+static int hv_iommu_get_caps(struct hv_output_get_iommu_capabilities *caps)
+{
+ u64 status;
+ unsigned long flags;
+ struct hv_input_get_iommu_capabilities *input;
+ struct hv_output_get_iommu_capabilities *output;
+
+ local_irq_save(flags);
+
+ input = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ output = *this_cpu_ptr(hyperv_pcpu_output_arg);
+ memset(input, 0, sizeof(*input));
+ input->partition_id = HV_PARTITION_ID_SELF;
+ status = hv_do_hypercall(HVCALL_GET_IOMMU_CAPABILITIES, input, output);
+ *caps = *output;
+
+ local_irq_restore(flags);
+
+ if (!hv_result_success(status))
+ hv_status_err(status, "\n");
+
+ return hv_result_to_errno(status);
+}
+
+static int __init hv_root_iommu_init(void)
+{
+ int rc;
+ struct iommu_device *iommup = &hv_virt_iommu;
+ struct hv_output_get_iommu_capabilities caps;
+
+ if (!hv_is_hyperv_initialized())
+ return -ENODEV;
+
+ rc = hv_iommu_get_caps(&caps);
+ if (rc)
+ return rc;
+
+ hv_iommu_max_iova = DMA_BIT_MASK(caps.max_iova_width);
+
+ /* This must come before iommu_device_register() because the latter
+ * calls into the hooks.
+ */
+ hv_initialize_special_domains();
+
+ rc = iommu_device_sysfs_add(iommup, NULL, NULL, "%s", "hyperv-iommu");
+ if (rc) {
+ pr_err("Hyper-V: iommu_device_sysfs_add failed: %d\n", rc);
+ return rc;
+ }
+
+ rc = iommu_device_register(iommup, &hv_iommu_ops, NULL);
+ if (rc) {
+ pr_err("Hyper-V: iommu_device_register failed: %d\n", rc);
+ goto err_sysfs_remove;
+ }
+
+ pr_info("Hyper-V IOMMU initialized\n");
+
+ return 0;
+
+err_sysfs_remove:
+ iommu_device_sysfs_remove(iommup);
+ return rc;
+}
+
+void __init hv_root_iommu_detect(void)
+{
+ if (no_iommu || iommu_detected || hv_l1vh_partition())
+ return;
+
+ if (!(ms_hyperv.misc_features & HV_DEVICE_DOMAIN_AVAILABLE))
+ return;
+
+ iommu_detected = 1;
+ x86_init.iommu.iommu_init = hv_root_iommu_init;
+
+ pci_request_acs();
+}
diff --git a/include/asm-generic/mshyperv.h b/include/asm-generic/mshyperv.h
index 2584e693d7b3..ad02d6956ae3 100644
--- a/include/asm-generic/mshyperv.h
+++ b/include/asm-generic/mshyperv.h
@@ -28,6 +28,9 @@
#define VTPM_BASE_ADDRESS 0xfed40000
+#define HV_REP_COUNT_MAX \
+ (HV_HYPERCALL_REP_COMP_MASK >> HV_HYPERCALL_REP_COMP_OFFSET)
+
enum hv_partition_type {
HV_PARTITION_TYPE_GUEST,
HV_PARTITION_TYPE_ROOT,
diff --git a/include/linux/hyperv.h b/include/linux/hyperv.h
index e85a1a043eac..8aa752e8c4e1 100644
--- a/include/linux/hyperv.h
+++ b/include/linux/hyperv.h
@@ -1823,4 +1823,10 @@ static inline unsigned long virt_to_hvpfn(void *addr)
#define HVPFN_DOWN(x) ((x) >> HV_HYP_PAGE_SHIFT)
#define page_to_hvpfn(page) (page_to_pfn(page) * NR_HV_HYP_PAGES_IN_PAGE)
+#ifdef CONFIG_HYPERV_ROOT_IOMMU
+void __init hv_root_iommu_detect(void);
+#else
+static inline void hv_root_iommu_detect(void) { }
+#endif /* CONFIG_HYPERV_ROOT_IOMMU */
+
#endif /* _HYPERV_H */
--
2.51.2.vfs.0.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs
2026-11-03 2:51 ` [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs Mukesh R
@ 2026-11-03 2:57 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-11-03 2:57 UTC (permalink / raw)
To: Mukesh R, linux-hyperv
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] iommu/hyperv: always-false NULL check on iommu_init in hyperv_init()
--
commit 4f2b2cb5f11d745054ce781b3dfdb1a7f85d3560
Author: Mukesh R <mrathor@linux.microsoft.com>
iommu/hyperv: Only call pviommu init on non-root VMs
This commit renames hv_iommu_init() to hv_pviommu_init() and updates
hyperv_init() so that x86_init.iommu.iommu_init is only assigned on non-root
Hyper-V VMs when x86_init.iommu.iommu_init is not already set.
> diff --git a/arch/x86/hyperv/hv_init.c b/arch/x86/hyperv/hv_init.c
> index b90e6f4875335..18651c38dba3e 100644
> --- a/arch/x86/hyperv/hv_init.c
> +++ b/arch/x86/hyperv/hv_init.c
> @@ -580,7 +580,8 @@ void __init hyperv_init(void)
> x86_init.timers.setup_percpu_clockev = hv_stimer_setup_percpu_clockev;
>
> #ifdef CONFIG_HYPERV_PVIOMMU
> - x86_init.iommu.iommu_init = hv_iommu_init;
> + if (!hv_root_partition() && x86_init.iommu.iommu_init == NULL)
> + x86_init.iommu.iommu_init = hv_pviommu_init;
[Severity: High]
Can x86_init.iommu.iommu_init == NULL ever evaluate to true here?
In arch/x86/kernel/x86_init.c, x86_init.iommu.iommu_init is statically
initialized to iommu_init_noop rather than NULL:
static int __init iommu_init_noop(void) { return 0; }
...
struct x86_init_ops x86_init __initdata = {
...
.iommu = {
.iommu_init = iommu_init_noop,
},
...
};
And in arch/x86/kernel/pci-dma.c, pci_iommu_init() invokes
x86_init.iommu.iommu_init() unconditionally without a NULL check.
Because x86_init.iommu.iommu_init defaults to iommu_init_noop (or is
overwritten by an earlier hardware IOMMU detector in pci_iommu_alloc()) and
is never set to NULL, doesn't checking x86_init.iommu.iommu_init == NULL in
hyperv_init() prevent hv_pviommu_init from ever being registered on non-root
guest VMs?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103025150.3192119-1-mrathor@linux.microsoft.com?part=4
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers
2026-11-03 2:51 ` [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers Mukesh R
@ 2026-11-03 3:17 ` Easwar Hariharan
0 siblings, 0 replies; 9+ messages in thread
From: Easwar Hariharan @ 2026-11-03 3:17 UTC (permalink / raw)
To: Mukesh R
Cc: linux-hyperv, linux-kernel, iommu, linux-arch, easwar.hariharan,
jgg, jacob.pan, haiyangz, wei.liu, decui, tglx, mingo, bp,
dave.hansen, x86, hpa, joro, will, robin.murphy, arnd
On 11/2/2026 18:51, Mukesh R wrote:
> Copy/import from Hyper-V public headers, definitions and hypercalls that
> are related to checking for device domain availibility, and mapping and
> unmapping of device pages.
>
> Signed-off-by: Mukesh R <mrathor@linux.microsoft.com>
> Reviewed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> ---
> include/hyperv/hvgdk_mini.h | 4 ++++
> include/hyperv/hvhdk_mini.h | 15 +++++++++++++++
> 2 files changed, 19 insertions(+)
>
> diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
> index 966945f0504f..606b16f43e16 100644
> --- a/include/hyperv/hvgdk_mini.h
> +++ b/include/hyperv/hvgdk_mini.h
> @@ -327,6 +327,8 @@ union hv_hypervisor_version_info {
> /* stimer Direct Mode is available */
> #define HV_STIMER_DIRECT_MODE_AVAILABLE BIT(19)
>
> +#define HV_DEVICE_DOMAIN_AVAILABLE BIT(24)
> +
> /*
> * Implementation recommendations. Indicates which behaviors the hypervisor
> * recommends the OS implement for optimal performance.
> @@ -489,6 +491,8 @@ union hv_vp_assist_msr_contents { /* HV_REGISTER_VP_ASSIST_PAGE */
> #define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_LIST 0x00b0
> #define HVCALL_CREATE_DEVICE_DOMAIN 0x00b1
> #define HVCALL_ATTACH_DEVICE_DOMAIN 0x00b2
> +#define HVCALL_MAP_DEVICE_GPA_PAGES 0x00b3
> +#define HVCALL_UNMAP_DEVICE_GPA_PAGES 0x00b4
> #define HVCALL_SIGNAL_EVENT_DIRECT 0x00c0
> #define HVCALL_POST_MESSAGE_DIRECT 0x00c1
> #define HVCALL_DISPATCH_VP 0x00c2
> diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
> index 57b70efe60d4..2bb757e00d86 100644
> --- a/include/hyperv/hvhdk_mini.h
> +++ b/include/hyperv/hvhdk_mini.h
> @@ -549,6 +549,7 @@ union hv_device_id { /* HV_DEVICE_ID */
> } __packed;
>
> /* Device domain types */
> +#define HV_DEVICE_DOMAIN_TYPE_S2 0 /* HV_DEVICE_DOMAIN_ID_TYPE_S2 */
> #define HV_DEVICE_DOMAIN_TYPE_S1 1 /* HV_DEVICE_DOMAIN_ID_TYPE_S1 */
>
> /* ID for default domain and NULL domain */
> @@ -657,6 +658,20 @@ struct hv_output_get_iommu_capabilities {
> u64 pgsize_bitmap;
> } __packed;
>
> +struct hv_input_map_device_gpa_pages { /* HV_INPUT_MAP_DEVICE_GPA_PAGES */
> +struct hv_input_device_domain device_domain;
Nit: Needs whitespace
> + union hv_input_vtl target_vtl;
> + u8 padding[3];
> + u32 map_flags;
> + u64 target_device_va_base;
> + u64 gpa_page_list[];
> +} __packed;
> +
> +struct hv_input_unmap_device_gpa_pages { /* HV_INPUT_UNMAP_DEVICE_GPA_PAGES */
> + struct hv_input_device_domain device_domain;
> + u64 target_device_va_base;
> +} __packed;
> +
> /* HV_LOGICAL_DEVICE_PROPERTY_CODE */
> enum hv_logical_device_property_code {
> HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU = 10,
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver
2026-11-03 2:51 ` [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver Mukesh R
@ 2026-11-03 3:25 ` sashiko-bot
0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-11-03 3:25 UTC (permalink / raw)
To: Mukesh R, linux-hyperv
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] iommu/hyperv: guest VM boot crash due to partition check in hv_root_iommu_detect()
- [High] x86/pci-dma: hv_root_iommu_init() overwritten by Intel/AMD IOMMU detect
- [High] iommu/hyperv: hv_iommu_att_dev2dom() fails on HV_STATUS_DEVICE_ALREADY_IN_DOMAIN
- [High] iommu/hyperv: tree corruption on zero or partial unmap in hv_iommu_unmap_pages()
- [Medium] iommu/hyperv: spurious hv_status_err() on memory deposit in hv_iommu_map_pgs()
- [Medium] iommu/hyperv: caller gfp_t flags ignored in hv_iommu_add_tree_mapping()
--
commit b814c086ae33fc3b6c22d26fe0a8b5dabc2d2f48
Author: Mukesh R <mrathor@linux.microsoft.com>
x86/hyperv: Implement root partition IOMMU kernel only driver
This commit adds a kernel-only virtual IOMMU driver for Microsoft Hyper-V
root partitions to manage paging domains, device attachment, and page
mappings via hypercalls. It hooks into pci_iommu_alloc() to detect and
initialize the Hyper-V root IOMMU during boot.
> diff --git a/arch/x86/kernel/pci-dma.c b/arch/x86/kernel/pci-dma.c
> index 75cf8f6ae8cd0..b383c4549615a 100644
> --- a/arch/x86/kernel/pci-dma.c
> +++ b/arch/x86/kernel/pci-dma.c
> @@ -8,6 +8,7 @@
> #include <linux/gfp.h>
> #include <linux/pci.h>
> #include <linux/amd-iommu.h>
> +#include <linux/hyperv.h>
>
> #include <asm/proto.h>
> #include <asm/dma.h>
> @@ -101,6 +102,7 @@ void __init pci_iommu_alloc(void)
> }
> pci_swiotlb_detect();
> gart_iommu_hole_init();
> + hv_root_iommu_detect();
> amd_iommu_detect();
> detect_intel_iommu();
> swiotlb_init(x86_swiotlb_enable, x86_swiotlb_flags);
[Severity: High]
When hv_root_iommu_detect() succeeds, it sets iommu_detected = 1 and
assigns x86_init.iommu.iommu_init = hv_root_iommu_init. However,
amd_iommu_detect() and detect_intel_iommu() are called unconditionally
right after it in pci_iommu_alloc().
In drivers/iommu/intel/dmar.c:detect_intel_iommu(), whenever an ACPI DMAR
table with valid DRHD units is present, the assignment to
x86_init.iommu.iommu_init happens outside the !iommu_detected check:
if (!iommu_detected && dmar_required()) {
iommu_detected = 1;
/* Make sure ACS will be enabled */
pci_request_acs();
}
x86_init.iommu.iommu_init = intel_iommu_init;
Even if the user boots with intel_iommu=off (as suggested in the Kconfig
help text) or firmware sets DMAR_REMAP_OPT_OUT, detect_intel_iommu() still
overwrites x86_init.iommu.iommu_init with intel_iommu_init, which then
sees dmar_policy_off() and exits without initializing any IOMMU.
Similarly, in drivers/iommu/amd/init.c:amd_iommu_detect(), if an ACPI IVRS
table is present and amd_iommu=off was not passed, iommu_go_to_state()
succeeds without checking iommu_detected:
ret = iommu_go_to_state(IOMMU_IVRS_DETECTED);
if (ret)
goto disable_snp;
iommu_detected = 1;
x86_init.iommu.iommu_init = amd_iommu_init;
This clobbers hv_root_iommu_init and later runs disable_iommus() against
the physical IOMMU registers owned by Hyper-V.
Won't amd_iommu_detect() and detect_intel_iommu() overwrite
x86_init.iommu.iommu_init on baremetal root partitions where ACPI DMAR or
IVRS tables are exposed?
[ ... ]
> diff --git a/drivers/iommu/hyperv/hv-iommu-root.c b/drivers/iommu/hyperv/hv-iommu-root.c
> new file mode 100644
> index 0000000000000..23212c9e44e49
> --- /dev/null
> +++ b/drivers/iommu/hyperv/hv-iommu-root.c
> @@ -0,0 +1,602 @@
[ ... ]
> +static int hv_iommu_add_tree_mapping(struct hv_iommu_domain *hvdom, ulong iova,
> + phys_addr_t paddr, size_t size, u32 flags)
> +{
> + ulong irqflags;
> + struct hv_iommu_mapping *mapping;
> +
> + mapping = kzalloc_obj(struct hv_iommu_mapping, GFP_ATOMIC);
> + if (!mapping)
> + return -ENOMEM;
[Severity: Medium]
As reported by Sashiko on v2 [1], hv_iommu_map_pages() receives a gfp_t
gfp parameter from the IOMMU core and passes it to
hv_deposit_memory_gfp(), but drops it when calling
hv_iommu_add_tree_mapping().
Since mapping is allocated before acquiring hvdom->mappings_lock, should
hv_iommu_add_tree_mapping() accept and use the caller's gfp flags instead
of hardcoding GFP_ATOMIC, which prevents memory reclaim and can fail with
-ENOMEM under memory pressure when called from sleepable GFP_KERNEL
contexts?
> +
> + mapping->paddr = paddr;
> + mapping->iova.start = iova;
> + mapping->iova.last = iova + size - 1;
> + mapping->flags = flags;
> +
> + spin_lock_irqsave(&hvdom->mappings_lock, irqflags);
> + interval_tree_insert(&mapping->iova, &hvdom->mappings_tree);
> + spin_unlock_irqrestore(&hvdom->mappings_lock, irqflags);
> +
> + return 0;
> +}
> +
> +/* If size == 0, then last = ULONG_MAX. With iova 0, will remove everything */
> +static size_t hv_iommu_del_tree_mappings(struct hv_iommu_domain *hvdom,
> + ulong iova, size_t size)
> +{
> + ulong flags;
> + size_t unmapped = 0;
> + ulong last = iova + size - 1;
> + struct hv_iommu_mapping *mapping = NULL;
> + struct interval_tree_node *node, *next;
> +
> + spin_lock_irqsave(&hvdom->mappings_lock, flags);
> + next = interval_tree_iter_first(&hvdom->mappings_tree, iova, last);
> + while (next) {
> + node = next;
> + mapping = container_of(node, struct hv_iommu_mapping, iova);
> + next = interval_tree_iter_next(node, iova, last);
> +
> + /* Splitting of a mapping is not supported at present */
> + if (mapping->iova.start < iova)
> + break;
> +
> + unmapped += mapping->iova.last - mapping->iova.start + 1;
> +
> + interval_tree_remove(node, &hvdom->mappings_tree);
> + kfree(mapping);
> + }
> + spin_unlock_irqrestore(&hvdom->mappings_lock, flags);
> +
> + return unmapped;
> +}
[ ... ]
> +static int hv_iommu_att_dev2dom(struct hv_iommu_domain *hvdom,
> + struct pci_dev *pdev)
> +{
> + ulong flags;
> + u64 status;
> + struct hv_input_attach_device_domain *input;
> +
> + local_irq_save(flags);
> + input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> + memset(input, 0, sizeof(*input));
> +
> + /* For null domain, hvdom->domid_num == HV_DEVICE_DOMAIN_ID_NULL */
> + input->device_domain.partition_id = HV_PARTITION_ID_SELF;
> + input->device_domain.domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
> + input->device_domain.domain_id.id = hvdom->domid_num;
> +
> + input->device_id.as_uint64 = hv_build_devid_type_pci(pdev);
> +
> + status = hv_do_hypercall(HVCALL_ATTACH_DEVICE_DOMAIN, input, NULL);
> + local_irq_restore(flags);
> +
> + if (!hv_result_success(status))
> + hv_status_err(status, "\n");
> +
> + return hv_result_to_errno(status);
> +}
> +
> +static int hv_iommu_attach_dev(struct iommu_domain *immdom, struct device *dev,
> + struct iommu_domain *old)
> +{
> + struct pci_dev *pdev;
> + int rc;
> + struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
> +
> + pdev = to_pci_dev(dev);
> +
> + rc = hv_iommu_att_dev2dom(hvdom, pdev);
> + if (rc)
> + WARN(1, "Failed to attach pdev:%s\n", pci_name(pdev));
> +
> + return rc;
> +}
[Severity: High]
In hv_iommu_att_dev2dom(), any hypercall status other than
HV_STATUS_SUCCESS is treated as an error, converted to -EEXIST on
HV_STATUS_DEVICE_ALREADY_IN_DOMAIN, and triggers the WARN(1) in
hv_iommu_attach_dev().
There are two normal paths where HVCALL_ATTACH_DEVICE_DOMAIN returns
HV_STATUS_DEVICE_ALREADY_IN_DOMAIN:
1. At boot, Hyper-V places root partition PCI devices in the Stage 2
default domain (HV_DEVICE_DOMAIN_ID_DEFAULT), which corresponds to
hv_def_identity_dom (IOMMU_DOMAIN_IDENTITY). When attaching a device to
hv_def_identity_dom (for example, when booting with
iommu.passthrough=1 or CONFIG_IOMMU_DEFAULT_PASSTHROUGH=y), the device
is already in HV_DEVICE_DOMAIN_ID_DEFAULT.
2. When an iommu_group created by pci_device_group() contains multiple PCI
devices sharing a DMA requester ID alias (such as devices behind a
PCIe-to-PCI/PCI-X bridge or multifunction devices with DMA alias
quirks), hv_build_devid_type_pci() resolves each device in the group to
the same hv_device_id via pci_for_each_dma_alias(). When
__iommu_group_set_domain_internal() iterates over group->devices
calling hv_iommu_attach_dev(), the second aliased device attempts to
attach the same hv_device_id to the same domain.
By comparison, drivers/iommu/hyperv/hv-iommu-guest.c:hv_iommu_attach_dev()
explicitly ignores HV_STATUS_DEVICE_ALREADY_IN_DOMAIN:
if (!hv_result_success(status) &&
hv_result(status) != HV_STATUS_DEVICE_ALREADY_IN_DOMAIN) {
hv_status_err(status, "HVCALL_ATTACH_DEVICE_DOMAIN failed\n");
return hv_result_to_errno(status);
}
Should hv_iommu_att_dev2dom() also treat
HV_STATUS_DEVICE_ALREADY_IN_DOMAIN as success?
[ ... ]
> +static size_t hv_iommu_unmap_pages(struct iommu_domain *immdom, ulong iova,
> + size_t pgsize, size_t pgcount,
> + struct iommu_iotlb_gather *gather)
> +{
> + struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
> + size_t unmap_sz, size = pgsize * pgcount;
> +
> + /* For valid input, the hypervisor guarantees unmap will succeed */
> + unmap_sz = hv_iommu_hyp_unmap_pages(immdom, iova, size);
> + if (unmap_sz != size)
> + WARN(1, "Failed to unmap device gpa(%lx/%lx)\n", unmap_sz,
> + size);
> +
> + size = hv_iommu_del_tree_mappings(hvdom, iova, unmap_sz);
> + if (size != unmap_sz)
> + pr_err("%s: could not delete tree mappings (%lx:%lx/%lx)\n",
> + __func__, iova, unmap_sz, size);
> +
> + return size;
> +}
[Severity: High]
As reported by Sashiko on v2 [1], hv_iommu_unmap_pages() and
hv_iommu_del_tree_mappings() can corrupt hvdom->mappings_tree when an
unmap fails, partially completes, or covers a sub-range of a multi-page
mapping:
1. In hv_iommu_del_tree_mappings(), last is computed as:
ulong last = iova + size - 1;
hv_iommu_domain_free() relies on iova == 0 and size == 0 wrapping last
to ULONG_MAX to delete all mappings in the tree. However,
hv_iommu_unmap_pages() passes unmap_sz directly to
hv_iommu_del_tree_mappings(hvdom, iova, unmap_sz) without checking if
unmap_sz == 0. If hv_iommu_hyp_unmap_pages() fails on the first batch
and returns 0 at iova == 0 (which is valid since aperture_start is 0),
last underflows to ULONG_MAX and hv_iommu_del_tree_mappings() deletes
every mapping in the domain even though nothing was unmapped in the
hypervisor. If iova > 0 and unmap_sz == 0, last wraps to iova - 1 and
interval_tree_iter_first(&hvdom->mappings_tree, iova, iova - 1) will
still match and delete any mapping spanning across iova - 1 and iova.
2. hv_iommu_map_pages() inserts a single struct hv_iommu_mapping node
covering [iova, iova + size - 1] for multi-page requests. In
hv_iommu_del_tree_mappings(), the loop checks:
if (mapping->iova.start < iova)
break;
but never checks if mapping->iova.last > last. If unmap_sz < size (due
to a partial hypercall unmap or a partial unmap of a multi-page node's
prefix), hv_iommu_del_tree_mappings() removes and frees the entire
hv_iommu_mapping node and returns its full size while
[last + 1, mapping->iova.last] remains mapped in the hypervisor.
3. Because hv_iommu_hyp_unmap_pages() runs before
hv_iommu_del_tree_mappings() validates the interval tree boundaries, an
unmap starting in the middle of an existing mapping
(mapping->iova.start < iova) will unmap the pages in the hypervisor
first, and then hv_iommu_del_tree_mappings() will break out of the loop
and return 0, leaving the stale mapping in hvdom->mappings_tree.
Could hv_iommu_unmap_pages() validate the range against
hvdom->mappings_tree before unmapping in the hypervisor, and guard against
unmap_sz == 0 and mapping->iova.last > last in
hv_iommu_del_tree_mappings()?
> +
> +/* Return: must return exact status from the hypercall without changes */
> +static u64 hv_iommu_map_pgs(struct hv_iommu_domain *hvdom, ulong iova,
> + phys_addr_t paddr, ulong npages, u32 map_flags)
> +{
> + u64 status;
> + int i;
> + struct hv_input_map_device_gpa_pages *input;
> + unsigned long flags, pfn;
> +
> + local_irq_save(flags);
> + input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> + memset(input, 0, sizeof(*input));
> +
> + input->device_domain.partition_id = HV_PARTITION_ID_SELF;
> + input->device_domain.domain_id.type = HV_DEVICE_DOMAIN_TYPE_S2;
> + input->device_domain.domain_id.id = hvdom->domid_num;
> + input->map_flags = map_flags;
> + input->target_device_va_base = iova;
> +
> + pfn = paddr >> HV_HYP_PAGE_SHIFT;
> + for (i = 0; i < npages; i++, pfn++)
> + input->gpa_page_list[i] = pfn;
> +
> + status = hv_do_rep_hypercall(HVCALL_MAP_DEVICE_GPA_PAGES, npages, 0,
> + input, NULL);
> + local_irq_restore(flags);
> +
> + if (!hv_result_success(status))
> + hv_status_err(status, "npgs:%lx paddr:%llx iova:%lx mf:0x%x\n",
> + npages, paddr, iova, map_flags);
> + return status;
> +}
> +
> +#define HV_MAP_DEVICE_GPA_BATCH_SIZE \
> + ((HV_HYP_PAGE_SIZE - sizeof(struct hv_input_map_device_gpa_pages)) \
> + / sizeof(u64))
> +
> +/* NB: cond_resched() is in vfio_iommu_map() */
> +static int hv_iommu_map_pages(struct iommu_domain *immdom, ulong iova,
> + phys_addr_t paddr, size_t pgsize, size_t pgcount,
> + int prot, gfp_t gfp, size_t *mapped)
> +{
> + u32 map_flags;
> + int ret;
> + u64 status = 0;
> + ulong npages, sav_iova = iova, done = 0;
> + struct hv_iommu_domain *hvdom = to_hv_domain(immdom);
> + size_t size = pgsize * pgcount;
> +
> + map_flags = HV_MAP_GPA_READABLE; /* required */
> + map_flags |= prot & IOMMU_WRITE ? HV_MAP_GPA_WRITABLE : 0;
> +
> + ret = hv_iommu_add_tree_mapping(hvdom, iova, paddr, size, map_flags);
> + if (ret)
> + return ret;
> +
> + npages = size >> HV_HYP_PAGE_SHIFT;
> + while (done < npages) {
> + ulong completed, remain = npages - done;
> +
> + remain = min(remain, HV_MAP_DEVICE_GPA_BATCH_SIZE);
> +
> + status = hv_iommu_map_pgs(hvdom, iova, paddr, remain,
> + map_flags);
> +
> + completed = hv_repcomp(status);
> + done = done + completed;
> + iova = iova + (completed << HV_HYP_PAGE_SHIFT);
> + paddr = paddr + (completed << HV_HYP_PAGE_SHIFT);
> +
> + if (hv_result_needs_memory(status)) {
> + ret = hv_deposit_memory_gfp(hv_current_partition_id,
> + status, gfp);
> + if (ret)
> + break;
> + continue;
> + }
> + if (!hv_result_success(status))
> + break;
> + }
> +
> + if (hv_result_success(status)) {
> + if (mapped)
> + *mapped = size;
> + } else {
> + size_t done_size = done << HV_HYP_PAGE_SHIFT;
> +
> + hv_status_err(status, "pgs:%lx/%lx iova:%lx\n", done, npages,
> + sav_iova);
> +
> + hv_iommu_del_tree_mappings(hvdom, sav_iova, size);
> +
> + /* Hypervisor guarantees unmap will succeed here */
> + hv_iommu_hyp_unmap_pages(immdom, sav_iova, done_size);
> +
> + if (mapped)
> + *mapped = 0;
> + }
> +
> + return hv_result_to_errno(status);
> +}
[Severity: Medium]
When HVCALL_MAP_DEVICE_GPA_PAGES returns a status where
hv_result_needs_memory(status) is true so that hv_iommu_map_pages() can
deposit memory via hv_deposit_memory_gfp() and retry, hv_iommu_map_pgs()
checks:
if (!hv_result_success(status))
hv_status_err(status, "npgs:%lx paddr:%llx iova:%lx mf:0x%x\n",
npages, paddr, iova, map_flags);
Won't this log a false KERN_ERR message every time the hypervisor requests
a normal memory deposit during DMA mapping, and log hv_status_err() twice
when a genuine hypercall failure occurs (once in hv_iommu_map_pgs() and
again in the error path of hv_iommu_map_pages())? Also, if
hv_deposit_memory_gfp() fails with a non-zero ret, its error code is
discarded at the end of hv_iommu_map_pages() in favor of
hv_result_to_errno(status).
[ ... ]
> +static int hv_iommu_get_caps(struct hv_output_get_iommu_capabilities *caps)
> +{
> + u64 status;
> + unsigned long flags;
> + struct hv_input_get_iommu_capabilities *input;
> + struct hv_output_get_iommu_capabilities *output;
> +
> + local_irq_save(flags);
> +
> + input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> + output = *this_cpu_ptr(hyperv_pcpu_output_arg);
> + memset(input, 0, sizeof(*input));
> + input->partition_id = HV_PARTITION_ID_SELF;
> + status = hv_do_hypercall(HVCALL_GET_IOMMU_CAPABILITIES, input, output);
> + *caps = *output;
> +
> + local_irq_restore(flags);
> +
> + if (!hv_result_success(status))
> + hv_status_err(status, "\n");
> +
> + return hv_result_to_errno(status);
> +}
[ ... ]
> +void __init hv_root_iommu_detect(void)
> +{
> + if (no_iommu || iommu_detected || hv_l1vh_partition())
> + return;
> +
> + if (!(ms_hyperv.misc_features & HV_DEVICE_DOMAIN_AVAILABLE))
> + return;
> +
> + iommu_detected = 1;
> + x86_init.iommu.iommu_init = hv_root_iommu_init;
> +
> + pci_request_acs();
> +}
[Severity: High]
CONFIG_HYPERV_ROOT_IOMMU defaults to CONFIG_HYPERV when CONFIG_MSHV_ROOT
is enabled, so a single kernel image can boot in both root partitions
(HV_PARTITION_TYPE_ROOT) and regular guest VMs (HV_PARTITION_TYPE_GUEST).
In hv_root_iommu_detect(), checking hv_l1vh_partition() only excludes
HV_PARTITION_TYPE_L1VH, not HV_PARTITION_TYPE_GUEST. When a regular guest
VM boots with HV_DEVICE_DOMAIN_AVAILABLE advertised in
ms_hyperv.misc_features (for example, when guest pvIOMMU is enabled),
hv_root_iommu_detect() will set iommu_detected = 1 (causing
hv_pviommu_init() to abort with -ENODEV) and register hv_root_iommu_init.
When pci_iommu_init() then runs hv_root_iommu_init() ->
hv_iommu_get_caps(), it dereferences hyperv_pcpu_output_arg:
output = *this_cpu_ptr(hyperv_pcpu_output_arg);
...
*caps = *output;
However, drivers/hv/hv_common.c:hv_common_init() only allocates
hyperv_pcpu_output_arg when hv_output_page_exists() is true:
/* Allocate the per-CPU state for output arg for root */
if (hv_output_page_exists()) {
hyperv_pcpu_output_arg = alloc_percpu(void *);
BUG_ON(!hyperv_pcpu_output_arg);
}
In a regular guest VM, hyperv_pcpu_output_arg is NULL, causing a NULL
pointer dereference during boot.
Should hv_root_iommu_detect() check !hv_root_partition() instead of
hv_l1vh_partition()?
[1] https://lore.kernel.org/r/20260929223622.778617-5-mrathor@linux.microsoft.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103025150.3192119-1-mrathor@linux.microsoft.com?part=5
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-11-03 3:25 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 2:51 [PATCH V3 0/5] Hyper-V: root VM iommu kernel only driver Mukesh R
2026-11-03 2:51 ` [PATCH V3 1/5] mshv: Add gfp flags to deposit memory logic Mukesh R
2026-11-03 2:51 ` [PATCH V3 2/5] PCI: hv: Export hv_build_devid_type_pci() and change return type Mukesh R
2026-11-03 2:51 ` [PATCH V3 3/5] mshv: Import data structs around device domains from hyperv headers Mukesh R
2026-11-03 3:17 ` Easwar Hariharan
2026-11-03 2:51 ` [PATCH V3 4/5] iommu/hyperv: Only call pviommu init on non-root VMs Mukesh R
2026-11-03 2:57 ` sashiko-bot
2026-11-03 2:51 ` [PATCH V3 5/5] x86/hyperv: Implement root partition IOMMU kernel only driver Mukesh R
2026-11-03 3:25 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.