Linux-HyperV List
 help / color / mirror / Atom feed
* Re: [PATCH net-next v11 2/2] net: mana: force full-page RX buffers via ethtool private flag
From: Simon Horman @ 2026-07-08 15:57 UTC (permalink / raw)
  To: dipayanroy
  Cc: Simon Horman, kys, haiyangz, wei.liu, decui, andrew+netdev, davem,
	edumazet, kuba, pabeni, leon, longli, kotaranov, shradhagupta,
	ssengar, ernis, shirazsaleem, linux-hyperv, netdev, linux-kernel,
	linux-rdma, stephen, jacob.e.keller, dipayanroy, leitao, kees,
	john.fastabend, hawk, bpf, daniel, ast, sdf, yury.norov,
	pavan.chebbi
In-Reply-To: <20260701141808.461554-3-dipayanroy@linux.microsoft.com>

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
net: mana: force full-page RX buffers via ethtool private flag

This adds an ethtool private flag "full-page-rx" that forces one RX buffer
per page to avoid a page_pool fragment path throughput regression on some
4K PAGE_SIZE ARM64 platforms. Toggling the flag while the port is up
triggers a mana_detach() + mana_attach() reconfiguration to apply the new
buffer layout.

A few questions inline.

> diff --git a/drivers/net/ethernet/microsoft/mana/mana_en.c b/drivers/net/ethernet/microsoft/mana/mana_en.c
> index 7438ea6b3f26..38f50b4eef87 100644
> --- a/drivers/net/ethernet/microsoft/mana/mana_en.c
> +++ b/drivers/net/ethernet/microsoft/mana/mana_en.c

[ ... mana_use_single_rxbuf_per_page() and mana_get_rxbuf_cfg() hunks
      unrelated to the comments below ... ]

> diff --git a/drivers/net/ethernet/microsoft/mana/mana_ethtool.c b/drivers/net/ethernet/microsoft/mana/mana_ethtool.c
> index 3531abfdaf43..24f6ce63b031 100644
> --- a/drivers/net/ethernet/microsoft/mana/mana_ethtool.c
> +++ b/drivers/net/ethernet/microsoft/mana/mana_ethtool.c

[ ... ]

> @@ -611,6 +630,88 @@ static int mana_get_link_ksettings(struct net_device *ndev,
>  	return 0;
>  }
>  
> +static u32 mana_get_priv_flags(struct net_device *ndev)
> +{
> +	struct mana_port_context *apc = netdev_priv(ndev);
> +
> +	return apc->priv_flags;
> +}
> +
> +static int mana_set_priv_flags(struct net_device *ndev, u32 priv_flags)
> +{
> +	struct mana_port_context *apc = netdev_priv(ndev);
> +	u32 changed = apc->priv_flags ^ priv_flags;
> +	u32 old_priv_flags = apc->priv_flags;
> +	bool schedule_port_reset = false;
> +	int err = 0;
> +
> +	if (!changed)
> +		return 0;
> +
> +	/* Reject unknown bits */
> +	if (priv_flags & ~GENMASK(MANA_PRIV_FLAG_MAX - 1, 0))
> +		return -EINVAL;
> +
> +	if (changed & BIT(MANA_PRIV_FLAG_USE_FULL_PAGE_RXBUF)) {
> +		apc->priv_flags = priv_flags;

[Low]
Is the storage of priv_flags intentionally nested inside the per-flag
branch?  With only one flag today this is fine, but when a second bit is
added to enum mana_priv_flag_bits, a user toggling only the new bit
passes the !changed and unknown-bits checks, the outer if is skipped,
apc->priv_flags is never updated, and mana_set_priv_flags() returns 0
while mana_get_priv_flags() keeps reporting the old state.

Would it be cleaner to store priv_flags unconditionally after validation
and use per-flag if (changed & BIT(...)) blocks only for the
flag-specific reconfiguration?

> +
> +		if (!apc->port_is_up) {
> +			/* Port is down, flag updated to apply on next up
> +			 * so just return.
> +			 */
> +			return 0;
> +		}
> +
> +		/* Pre-allocate buffers to prevent failure in mana_attach
> +		 * later
> +		 */
> +		err = mana_pre_alloc_rxbufs(apc, ndev->mtu, apc->num_queues);

[Low]
When the MTU is already jumbo (mtu + MANA_RXBUF_PAD > PAGE_SIZE / 2) or
XDP is attached, mana_use_single_rxbuf_per_page() already returns true
regardless of the new priv flag:

	if (apc->priv_flags & BIT(MANA_PRIV_FLAG_USE_FULL_PAGE_RXBUF))
		return true;

	/* For xdp and jumbo frames make sure only one packet fits per page. */
	if (mtu + MANA_RXBUF_PAD > PAGE_SIZE / 2 || mana_xdp_get(apc))
		return true;

In that case mana_get_rxbuf_cfg() produces identical output whether the
new flag is on or off, yet toggling the flag here always performs a full
mana_pre_alloc_rxbufs() + mana_detach() + mana_attach() cycle and flaps
the link.

Would a pre/post comparison of mana_get_rxbuf_cfg() output be worth
adding to skip the reconfiguration when nothing actually changes?

[High]
mana_set_priv_flags() drives the same detach/attach sequence used by
mana_set_channels() and mana_per_port_queue_reset_work_handler(), but
does not take apc->vport_mutex and set apc->channel_changing = true
across the window.

The struct comment on channel_changing spells out the invariant:

	/* Set by mana_set_channels() under vport_mutex to block RDMA
	 * from grabbing the vport during the detach/attach window.
	 * Checked by mana_cfg_vport() when called from the RDMA path.
	 */
	bool channel_changing;

Without it, after mana_detach() uncfg's the vport an RDMA client can
call mana_cfg_vport() and bump vport_use_count, and then mana_attach()
will fail with -EBUSY when it re-cfg's the vport.

There is also no equivalent of the '!port_is_up && vport_use_count'
early -EBUSY check that mana_set_channels() performs, so is the flag
allowed to be changed silently while an RDMA client is using the vport?

> +		if (err) {
> +			netdev_err(ndev,
> +				   "Insufficient memory for new allocations\n");
> +			apc->priv_flags = old_priv_flags;
> +			return err;
> +		}
> +
> +		err = mana_detach(ndev, false);

[High]
mana_detach() begins with ASSERT_RTNL(), and so does mana_attach()
called below.  Is RTNL guaranteed to be held on the SET priv-flags path?

mana registers net_shaper_ops in mana_devops, which makes it an
ops-locked driver, so netdev_need_ops_lock() is true and the ethtool
core acquires rtnl only when the driver opts in via
ETHTOOL_OP_NEEDS_RTNL_* for that specific command.

Looking at mana_ethtool_ops.op_needs_rtnl:

	.op_needs_rtnl		= ETHTOOL_OP_NEEDS_RTNL_SCHANNELS |
				  ETHTOOL_OP_NEEDS_RTNL_SRINGPARAM |
				  ETHTOOL_OP_NEEDS_RTNL_GLINK,

ETHTOOL_OP_NEEDS_RTNL_SPFLAGS is not listed, so ASSERT_RTNL() in the
detach/attach helpers below will WARN, and the code races with
RTNL-serialized mutators (mana_open, mana_close, mana_change_mtu,
mana_tx_timeout) that read and write apc->port_is_up, apc->rxqs,
apc->tx_qp and apc->port_st_save.

Should ETHTOOL_OP_NEEDS_RTNL_SPFLAGS be added to op_needs_rtnl, matching
what is already done for SET channels?

> +		if (err) {
> +			netdev_err(ndev, "mana_detach failed: %d\n", err);
> +			apc->priv_flags = old_priv_flags;
> +
> +			/* Port is in an inconsistent state. Restore
> +			 * 'port_is_up' so that queue reset work handler
> +			 * can properly detach and re-attach.
> +			 */
> +			apc->port_is_up = true;
> +			schedule_port_reset = true;
> +			goto out;
> +		}
> +
> +		err = mana_attach(ndev);
> +		if (err) {
> +			netdev_err(ndev, "mana_attach failed: %d\n", err);
> +			apc->priv_flags = old_priv_flags;
> +
> +			/* Restore 'port_is_up' so the reset work handler
> +			 * can properly detach/attach. Without this,
> +			 * the handler sees port_is_up=false and skips
> +			 * queue allocation, leaving the port dead.
> +			 */
> +			apc->port_is_up = true;
> +			schedule_port_reset = true;

[High]
On this branch, mana_detach() has already succeeded and run
mana_cleanup_port_context(), so apc->rxqs is NULL and queues are torn
down.  Setting apc->port_is_up = true here between now and when
queue_reset_work runs opens a window where callers gate on port_is_up
as their "safe to access rxqs/tx_qp" predicate.

For example, mana_get_ethtool_stats() does:

	if (!apc->port_is_up)
		return;
	...
	rxq = apc->rxqs[q];
	...

A concurrent ethtool -S invocation during that window will pass the
port_is_up gate and dereference apc->rxqs[q]->stats on a NULL rxqs.

Is the port_is_up restore actually needed for the reset work handler's
mana_detach() call?  On the reset path, mana_detach() takes its early
return when !netif_device_present(ndev):

	if (!from_close && !netif_device_present(ndev))
		return 0;

That early return does not touch apc->port_st_save, so the saved state
from the earlier successful mana_detach() should already be intact for
the follow-up mana_attach().

There is also no smp_wmb() paired with this write, unlike the pattern
used inside mana_detach()/mana_attach() proper.

> +		}
> +	}
> +
> +out:
> +	mana_pre_dealloc_rxbufs(apc);
> +
> +	if (schedule_port_reset)
> +		queue_work(apc->ac->per_port_queue_reset_wq,
> +			   &apc->queue_reset_work);
> +
> +	return err;
> +}
> +

[ ... remaining hunks unrelated to the comments above ... ]

^ permalink raw reply

* RE: [EXTERNAL] Re: [PATCH net-next v2] net: mana: Add handler for sriov configure
From: Haiyang Zhang @ 2026-07-08 20:52 UTC (permalink / raw)
  To: Paolo Abeni, haiyangz@linux.microsoft.com
  Cc: linux-hyperv@vger.kernel.org, netdev@vger.kernel.org,
	KY Srinivasan, wei.liu@kernel.org, Dexuan Cui, Long Li,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, horms@kernel.org, ernis@linux.microsoft.com,
	dipayanroy@linux.microsoft.com, gargaditya@linux.microsoft.com,
	shradhagupta@linux.microsoft.com, linux-kernel@vger.kernel.org,
	Paul Rosswurm
In-Reply-To: <20260708080854.64655-1-pabeni@redhat.com>



> -----Original Message-----
> From: Paolo Abeni <pabeni@redhat.com>
> Sent: Wednesday, July 8, 2026 4:09 AM
> To: haiyangz@linux.microsoft.com
> Cc: linux-hyperv@vger.kernel.org; netdev@vger.kernel.org; KY Srinivasan
> <kys@microsoft.com>; Haiyang Zhang <haiyangz@microsoft.com>;
> wei.liu@kernel.org; Dexuan Cui <DECUI@microsoft.com>; Long Li
> <longli@microsoft.com>; andrew+netdev@lunn.ch; davem@davemloft.net;
> edumazet@google.com; kuba@kernel.org; pabeni@redhat.com; horms@kernel.org;
> ernis@linux.microsoft.com; dipayanroy@linux.microsoft.com;
> gargaditya@linux.microsoft.com; shradhagupta@linux.microsoft.com; linux-
> kernel@vger.kernel.org; Paul Rosswurm <paulros@microsoft.com>
> Subject: [EXTERNAL] Re: [PATCH net-next v2] net: mana: Add handler for
> sriov configure
> 
> From: AI Reviewer <ai@example.com>
> 
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> Full review at:
> https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fsashiko.
> dev%2F&data=05%7C02%7Chaiyangz%40microsoft.com%7C27add93a1c4b4c0c7b4808ded
> cc833d4%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C639190949593984694%7C
> Unknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJX
> aW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=qLm%2BrL1nEF6%
> 2FlF2%2Fmed2mnc%2BVR4Vp9P6G6i5Im1PquU%3D&reserved=0
> ---
> net: mana: Add handler for sriov configure
> 
> Add callback function for the pci_driver / sriov_configure.
> 
> It asks the NIC to provide certain number of VFs, or disable
> VFs if the request is zero.
> 
> > diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c
> b/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > index e8b7ffb47eb98..f7a79874fd0eb 100644
> > --- a/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > +++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > @@ -2496,6 +2496,20 @@ static void mana_gd_shutdown(struct pci_dev
> *pdev)
> >  	pci_disable_device(pdev);
> >  }
> 
> [Severity: High]
> With the addition of SR-IOV support in this patch, do mana_gd_shutdown()
> and
> mana_gd_remove() need to call pci_disable_sriov()?
> 
> If a user enables VFs via sysfs and then unbinds the driver or shuts down
> the
> system, could this omission introduce a regression where orphaned, dead VF
> PCI
> devices are left in the kernel?
> 
> Accessing these orphaned VFs might trigger use-after-free crashes or
> hardware
> faults, as the underlying PF hardware and driver context have been
> destroyed.
Will fix.

> 
> > +static int mana_sriov_configure(struct pci_dev *pdev, int numvfs)
> > +{
> > +	int err = 0;
> > +
> > +	dev_info(&pdev->dev, "Requested num VFs: %d\n", numvfs);
> > +
> > +	if (numvfs > 0)
> > +		err = pci_enable_sriov(pdev, numvfs);
> > +	else
> > +		pci_disable_sriov(pdev);
> 
> [Severity: High]
> Should this path check if the VFs are currently assigned to a guest VM
> before
> disabling SR-IOV?
> 
> If a privileged user writes 0 to sriov_numvfs while VFs are actively
> assigned
> to a VM (e.g., via vfio-pci), could forcefully removing the PCI devices
> cause
> a regression resulting in host IOMMU faults and guest kernel panics?
> 
> Would it be better to check pci_vfs_assigned(pdev) here, or use the
> pci_sriov_configure_simple() helper to safely enforce this?

Will update.
I will send out an updated patch soon.

Thanks,
- Haiyang

^ permalink raw reply

* [PATCH net-next v3] net: mana: Add handler for sriov configure
From: Haiyang Zhang @ 2026-07-08 20:59 UTC (permalink / raw)
  To: linux-hyperv, netdev, K. Y. Srinivasan, Haiyang Zhang, Wei Liu,
	Dexuan Cui, Long Li, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Erni Sri Satya Vennela,
	Dipayaan Roy, Aditya Garg, Shradha Gupta, linux-kernel
  Cc: paulros

From: Haiyang Zhang <haiyangz@microsoft.com>

Add callback function for the pci_driver / sriov_configure.

It asks the NIC to provide certain number of VFs, or disable
VFs if the request is zero.

Signed-off-by: Haiyang Zhang <haiyangz@microsoft.com>
---
v3:
  Updated sriov disabling paths suggested by Paolo Abeni

v2:
  No longer change VF autoprobe as discussed with Leon Romanovsky and Bjorn Helgaas.

---
 .../net/ethernet/microsoft/mana/gdma_main.c   | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c b/drivers/net/ethernet/microsoft/mana/gdma_main.c
index aef3b77229c1..80a9118a90bc 100644
--- a/drivers/net/ethernet/microsoft/mana/gdma_main.c
+++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c
@@ -2456,6 +2456,8 @@ static void mana_gd_remove(struct pci_dev *pdev)
 {
 	struct gdma_context *gc = pci_get_drvdata(pdev);
 
+	pci_disable_sriov(pdev);
+
 	mana_rdma_remove(&gc->mana_ib);
 	mana_remove(&gc->mana, false);
 
@@ -2517,6 +2519,8 @@ static void mana_gd_shutdown(struct pci_dev *pdev)
 
 	dev_info(&pdev->dev, "Shutdown was called\n");
 
+	pci_disable_sriov(pdev);
+
 	mana_rdma_remove(&gc->mana_ib);
 	mana_remove(&gc->mana, true);
 
@@ -2525,6 +2529,27 @@ static void mana_gd_shutdown(struct pci_dev *pdev)
 	pci_disable_device(pdev);
 }
 
+static int mana_sriov_configure(struct pci_dev *pdev, int numvfs)
+{
+	int err = 0;
+
+	dev_info(&pdev->dev, "Requested num VFs: %d\n", numvfs);
+
+	if (numvfs > 0) {
+		err = pci_enable_sriov(pdev, numvfs);
+	} else {
+		if (pci_vfs_assigned(pdev)) {
+			dev_warn(&pdev->dev,
+				 "Cannot disable SR-IOV while VFs are assigned\n");
+			return -EPERM;
+		}
+
+		pci_disable_sriov(pdev);
+	}
+
+	return err ? err : numvfs;
+}
+
 static const struct pci_device_id mana_id_table[] = {
 	{ PCI_DEVICE(PCI_VENDOR_ID_MICROSOFT, MANA_PF_DEVICE_ID) },
 	{ PCI_DEVICE(PCI_VENDOR_ID_MICROSOFT, MANA_PF2_DEVICE_ID) },
@@ -2540,6 +2565,7 @@ static struct pci_driver mana_driver = {
 	.suspend	= mana_gd_suspend,
 	.resume		= mana_gd_resume,
 	.shutdown	= mana_gd_shutdown,
+	.sriov_configure = mana_sriov_configure,
 };
 
 static int __init mana_driver_init(void)
-- 
2.34.1


^ permalink raw reply related

* [PATCH] mshv: bounds-check cpu index in vtl mmap fault handler
From: Yi Xie @ 2026-07-09  2:19 UTC (permalink / raw)
  To: kys, haiyangz, wei.liu, decui, longli; +Cc: linux-hyperv, linux-kernel, Yi Xie

cpu is taken from pgoff & 0xffff.  cpu_online() does not reject cpu >=
nr_cpu_ids, and per_cpu_ptr() can then walk off __per_cpu_offset.

Signed-off-by: Yi Xie <xieyi@kylinos.cn>
---
 drivers/hv/mshv_vtl_main.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hv/mshv_vtl_main.c b/drivers/hv/mshv_vtl_main.c
index 0d3d4161974f..fc50c44ac1bd 100644
--- a/drivers/hv/mshv_vtl_main.c
+++ b/drivers/hv/mshv_vtl_main.c
@@ -801,7 +801,7 @@ static vm_fault_t mshv_vtl_fault(struct vm_fault *vmf)
 	int cpu = vmf->pgoff & MSHV_PG_OFF_CPU_MASK;
 	int real_off = vmf->pgoff >> MSHV_REAL_OFF_SHIFT;
 
-	if (!cpu_online(cpu))
+	if (cpu >= nr_cpu_ids || !cpu_online(cpu))
 		return VM_FAULT_SIGBUS;
 	/*
 	 * CPU Hotplug is not supported in VTL2 in OpenHCL, where this kernel driver exists.
-- 
2.34.1

^ permalink raw reply related

* Re: [PATCH] mshv: bounds-check cpu index in vtl mmap fault handler
From: sashiko-bot @ 2026-07-09  2:51 UTC (permalink / raw)
  To: Yi Xie; +Cc: linux-hyperv
In-Reply-To: <20260709021947.49436-1-xieyi@kylinos.cn>

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:

Pre-existing issues:
- [High] Returning VM_FAULT_NOPAGE for unmapped/invalid offsets in mshv_vtl_fault creates an infinite page fault loop.
- [High] A partial allocation failure of `reg_page` on a secondary CPU leads to a NULL pointer dereference upon mmap fault, due to global state mismatch.
- [High] Registering the character device before CPU contexts are initialized exposes uninitialized memory or causes kernel panics.
- [High] The module leaks an eventfd wait queue entry, leading to a guaranteed Use-After-Free (UAF) upon module unload.
- [High] Missing allocation failure checks lead to a NULL pointer dereference in `mshv_vtl_hvcall_call`.
- [Medium] The CPU bounds check in `mshv_vtl_ioctl_set_poll_file` uses `num_possible_cpus()` instead of `nr_cpu_ids`, potentially rejecting valid CPUs.
--

--- Patch [1]: [PATCH] mshv: bounds-check cpu index in vtl mmap fault handler ---
commit 8ac0cc752b2b67def52fafd91c16c26937bc5c16
Author: Yi Xie <xieyi@kylinos.cn>

mshv: bounds-check cpu index in vtl mmap fault handler

cpu is taken from pgoff & 0xffff.  cpu_online() does not reject cpu >=
nr_cpu_ids, and per_cpu_ptr() can then walk off __per_cpu_offset.

> diff --git a/drivers/hv/mshv_vtl_main.c b/drivers/hv/mshv_vtl_main.c
> index 0d3d4161974f8..fc50c44ac1bda 100644
> --- a/drivers/hv/mshv_vtl_main.c
> +++ b/drivers/hv/mshv_vtl_main.c
> @@ -801,7 +801,7 @@ static vm_fault_t mshv_vtl_fault(struct vm_fault *vmf)
>  	int cpu = vmf->pgoff & MSHV_PG_OFF_CPU_MASK;
>  	int real_off = vmf->pgoff >> MSHV_REAL_OFF_SHIFT;
>  
> -	if (!cpu_online(cpu))
> +	if (cpu >= nr_cpu_ids || !cpu_online(cpu))
>  		return VM_FAULT_SIGBUS;

[Severity: High]
This is a pre-existing issue, but returning VM_FAULT_NOPAGE for unmapped or
invalid offsets in mshv_vtl_fault() can create an infinite page fault loop:

drivers/hv/mshv_vtl_main.c:mshv_vtl_fault() {
...
	if (real_off == MSHV_RUN_PAGE_OFFSET) {
		page = virt_to_page(mshv_vtl_cpu_run(cpu));
	} else if (real_off == MSHV_REG_PAGE_OFFSET) {
		if (!mshv_has_reg_page)
			return VM_FAULT_SIGBUS;
		page = mshv_vtl_cpu_reg_page(cpu);
	} else {
		return VM_FAULT_NOPAGE;
	}
...

Since VM_FAULT_NOPAGE indicates the fault was successfully handled without
installing a PTE, won't the CPU infinitely re-fault on the same instruction
and lock up the core if userspace accesses an unmapped offset? Should this
return VM_FAULT_SIGBUS instead?


[Severity: High]
This is a pre-existing issue, but does a partial allocation failure on a
secondary CPU lead to a NULL pointer dereference upon mmap fault?

If alloc_page() fails in mshv_vtl_configure_reg_page() for a specific CPU,
it returns without initializing the per-CPU reg_page:

drivers/hv/mshv_vtl_main.c:mshv_vtl_configure_reg_page() {
...
	reg_page = alloc_page(GFP_KERNEL | __GFP_ZERO | __GFP_RETRY_MAYFAIL);
	if (!reg_page) {
		WARN(1, "failed to allocate register page\n");
		return;
	}
...

However, the fault handler relies on the global mshv_has_reg_page flag
instead of checking if the per-CPU page is actually valid:

drivers/hv/mshv_vtl_main.c:mshv_vtl_fault() {
...
	} else if (real_off == MSHV_REG_PAGE_OFFSET) {
		if (!mshv_has_reg_page)
			return VM_FAULT_SIGBUS;
		page = mshv_vtl_cpu_reg_page(cpu);
	}
...
	get_page(page);

If the global flag is set by another CPU's successful allocation, won't
mshv_vtl_fault() bypass the safety check and call get_page(NULL) for the
failing CPU?


[Severity: High]
This is a pre-existing issue, but does registering the character device
before CPU contexts are initialized expose uninitialized memory?

drivers/hv/mshv_vtl_main.c:mshv_vtl_init() {
...
	ret = misc_register(&mshv_dev);
	if (ret) {
...
	ret = hv_vtl_setup_synic();
...

Since misc_register() exposes /dev/mshv to userspace before
hv_vtl_setup_synic() allocates the per-CPU run and reg_page pointers, can a
fast userspace process mmap the device and trigger a page fault resulting in
virt_to_page(NULL) or get_page(NULL)?


[Severity: Medium]
This is a pre-existing issue, but does the bounds check in
mshv_vtl_ioctl_set_poll_file() incorrectly reject valid CPUs on sparse
topologies?

drivers/hv/mshv_vtl_main.c:mshv_vtl_ioctl_set_poll_file() {
...
	if (input.cpu >= num_possible_cpus() || !cpu_online(input.cpu))
		return -EINVAL;
...

If the system has a sparse CPU possible mask, num_possible_cpus() could be
lower than a valid CPU ID. Should this use nr_cpu_ids instead?


[Severity: High]
This is a pre-existing issue, but does the module leak an eventfd wait queue
entry leading to a use-after-free upon module unload?

drivers/hv/mshv_vtl_main.c:mshv_vtl_ioctl_set_poll_file() {
...
	if (file) {
		init_waitqueue_func_entry(&poll_file->wait, mshv_vtl_poll_file_wake);
		init_poll_funcptr(&poll_file->pt, mshv_vtl_ptable_queue_proc);
		vfs_poll(file, &poll_file->pt);
	}
...

drivers/hv/mshv_vtl_main.c:mshv_vtl_exit() {
...
	device_del(mem_dev);
	kfree(mem_dev);
	misc_deregister(&mshv_vtl_low);
...

The MSHV_SET_POLL_FILE ioctl permanently adds the global per-CPU
poll_file->wait entry into the user-provided eventfd's wait queue. Since
mshv_vtl_exit() doesn't remove the wait queue entry or release the eventfd
reference, won't signaling the eventfd after module unload cause the kernel
to traverse the wait queue and dereference freed memory?


[Severity: High]
This is a pre-existing issue, but are missing allocation failure checks
causing a NULL pointer dereference in mshv_vtl_hvcall_call()?

drivers/hv/mshv_vtl_main.c:mshv_vtl_hvcall_call() {
...
	in = (void *)__get_free_page(GFP_KERNEL);
	out = (void *)__get_free_page(GFP_KERNEL);

	if (copy_from_user(in, (void __user *)hvcall.input_ptr, hvcall.input_size)) {
...

If __get_free_page() returns 0 (NULL) due to memory pressure, the
subsequent copy_from_user() will attempt to copy user-supplied data
to a NULL pointer. Shouldn't the return values of __get_free_page() be
validated?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260709021947.49436-1-xieyi@kylinos.cn?part=1

^ permalink raw reply

* Re: [PATCH net v3 0/2] Fix MANA RX with bounce buffering
From: patchwork-bot+netdevbpf @ 2026-07-09  8:40 UTC (permalink / raw)
  To: Dexuan Cui
  Cc: kys, haiyangz, wei.liu, longli, andrew+netdev, davem, edumazet,
	kuba, pabeni, kotaranov, horms, ernis, dipayanroy, kees,
	jacob.e.keller, ssengar, linux-hyperv, netdev, linux-kernel,
	linux-rdma, stable
In-Reply-To: <20260702041237.617719-1-decui@microsoft.com>

Hello:

This series was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:

On Wed,  1 Jul 2026 21:12:35 -0700 you wrote:
> With swiotlb=force, the MANA NIC fails to work properly due to commit
> 730ff06d3f5c ("net: mana: Use page pool fragments for RX buffers instead
> of full pages to improve memory efficiency.").
> 
> This happens because, with the standard MTU=1500, the aforementioned
> commit uses page pool frags with PP_FLAG_DMA_MAP, but fails to call
> page_pool_dma_sync_for_cpu() to sync the received packet for CPU acces
> before handing the RX buffer to the stack.
> 
> [...]

Here is the summary with links:
  - [net,v3,1/2] net: mana: Validate the packet length reported by the NIC
    https://git.kernel.org/netdev/net/c/2e2a83b4998a
  - [net,v3,2/2] net: mana: Sync page pool RX frags for CPU
    https://git.kernel.org/netdev/net/c/c72a0f09c57f

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply

* Re: [PATCH net-next v5] net: mana: Add Interrupt Moderation support
From: patchwork-bot+netdevbpf @ 2026-07-09  9:00 UTC (permalink / raw)
  To: Haiyang Zhang
  Cc: linux-hyperv, netdev, kys, haiyangz, wei.liu, decui, longli,
	andrew+netdev, davem, edumazet, kuba, pabeni, kotaranov, horms,
	ernis, dipayanroy, shradhagupta, gargaditya, sdf, leitao,
	linux-kernel, linux-rdma, paulros
In-Reply-To: <20260702220123.815018-1-haiyangz@linux.microsoft.com>

Hello:

This patch was applied to netdev/net-next.git (main)
by Paolo Abeni <pabeni@redhat.com>:

On Thu,  2 Jul 2026 15:01:04 -0700 you wrote:
> From: Haiyang Zhang <haiyangz@microsoft.com>
> 
> Add Static and Dynamic Interrupt Moderation (DIM) support for
> Rx and Tx.
> Update queue creation procedure with new data struct with the related
> settings.
> Add functions to collect stat for DIM, and workers to update DIM data
> and settings.
> Update ethtool handler to get/set the moderation settings from a user.
> To avoid detach/re-attach ops, ring DIM doorbell to change settings
> at run time.
> By default, adaptive-rx/tx (DIM) are enabled if supported by HW.
> 
> [...]

Here is the summary with links:
  - [net-next,v5] net: mana: Add Interrupt Moderation support
    https://git.kernel.org/netdev/net-next/c/433f482add31

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply

* Re: [PATCH v2 2/4] Drivers: hv: Add logical device ID registry for vPCI devices
From: Yu Zhang @ 2026-07-09 11:33 UTC (permalink / raw)
  To: Michael Kelley
  Cc: linux-kernel@vger.kernel.org, linux-hyperv@vger.kernel.org,
	iommu@lists.linux.dev, linux-pci@vger.kernel.org,
	linux-arch@vger.kernel.org, wei.liu@kernel.org, kys@microsoft.com,
	haiyangz@microsoft.com, decui@microsoft.com, longli@microsoft.com,
	joro@8bytes.org, will@kernel.org, robin.murphy@arm.com,
	bhelgaas@google.com, kwilczynski@kernel.org,
	lpieralisi@kernel.org, mani@kernel.org, robh@kernel.org,
	arnd@arndb.de, jgg@ziepe.ca, jacob.pan@linux.microsoft.com,
	tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <BN7PR02MB4148C8DB748CFD863B97D1B7D4FF2@BN7PR02MB4148.namprd02.prod.outlook.com>

On Wed, Jul 08, 2026 at 01:28:42PM +0000, Michael Kelley wrote:
> From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Wednesday, July 8, 2026 6:09 AM
> > 
> > On Wed, Jul 08, 2026 at 02:52:43AM +0000, Michael Kelley wrote:
> > > From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> > > >
> > > > From: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > > >
> > > > Hyper-V identifies each PCI pass-thru device by a logical device ID in
> > > > its hypercall interface. This ID consists of a per-bus prefix, derived
> > > > from the VMBus device instance GUID, combined with the PCI function
> > > > number of the endpoint device.
> > > >
> > > > Add a small registry in hv_common.c that maps a PCI domain number to its
> > > > logical device ID prefix. The vPCI bus driver (pci-hyperv) registers the
> > > > prefix when a bus is probed and unregisters it when the bus is removed.
> > > > Consumers such as the para-virtualized IOMMU driver look up the prefix
> > > > by PCI domain number and combine it with the function number to form the
> > > > complete logical device ID for hypercalls.
> > > >
> > > > The prefix construction is shared via hv_build_logical_dev_id_prefix() so
> > > > that pci-hyperv's interrupt retargeting path and the registry use exactly
> > > > the same byte layout. It is derived on demand from the constant hv_device
> > > > instance GUID rather than cached in struct hv_pcibus_device, which is
> > > > private to the pci-hyperv module; this keeps the interface narrow and
> > > > avoids depending on pci-hyperv internals.
> > > >
> > > > Co-developed-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > > > Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > > > Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > > > ---
> > > >  drivers/hv/hv_common.c              | 95 +++++++++++++++++++++++++++++
> > > >  drivers/pci/controller/pci-hyperv.c | 21 +++++--
> > > >  include/asm-generic/mshyperv.h      | 13 ++++
> > > >  include/linux/hyperv.h              |  8 +++
> > > >  4 files changed, 132 insertions(+), 5 deletions(-)
> > > >
> > > > diff --git a/drivers/hv/hv_common.c b/drivers/hv/hv_common.c
> > > > index 6b67ac616789..53493f8d14dc 100644
> > > > --- a/drivers/hv/hv_common.c
> > > > +++ b/drivers/hv/hv_common.c
> > > > @@ -26,6 +26,8 @@
> > > >  #include <linux/kmsg_dump.h>
> > > >  #include <linux/sizes.h>
> > > >  #include <linux/slab.h>
> > > > +#include <linux/list.h>
> > > > +#include <linux/spinlock.h>
> > > >  #include <linux/dma-map-ops.h>
> > > >  #include <linux/set_memory.h>
> > > >  #include <hyperv/hvhdk.h>
> > > > @@ -863,3 +865,96 @@ const char *hv_result_to_string(u64 status)
> > > >  	return "Unknown";
> > > >  }
> > > >  EXPORT_SYMBOL_GPL(hv_result_to_string);
> > > > +
> > > > +#ifdef CONFIG_HYPERV_PVIOMMU
> > > > +/*
> > > > + * Logical device ID registry shared between the vPCI bus driver
> > > > + * (pci-hyperv) and the para-virtualized IOMMU driver. The vPCI driver
> > > > + * registers the per-bus logical device ID prefix at bus probe time, and
> > > > + * the pvIOMMU driver looks it up to build the full logical device ID used
> > > > + * in IOMMU hypercalls.
> > > > + */
> > > > +struct hv_pci_busdata {
> > > > +	int		 pci_domain_nr;
> > > > +	u32		 logical_dev_id_prefix;
> > > > +	struct list_head list;
> > > > +};
> > > > +
> > > > +static LIST_HEAD(hv_pci_bus_list);
> > > > +static DEFINE_SPINLOCK(hv_pci_bus_lock);
> > > > +
> > > > +int hv_iommu_register_pci_bus(int pci_domain_nr, u32 logical_dev_id_prefix)
> > > > +{
> > > > +	struct hv_pci_busdata *bus, *new;
> > > > +	int ret = 0;
> > > > +
> > > > +	new = kzalloc_obj(*new, GFP_KERNEL);
> > > > +	if (!new)
> > > > +		return -ENOMEM;
> > > > +
> > > > +	spin_lock(&hv_pci_bus_lock);
> > > > +	list_for_each_entry(bus, &hv_pci_bus_list, list) {
> > > > +		if (bus->pci_domain_nr != pci_domain_nr)
> > > > +			continue;
> > > > +
> > > > +		if (bus->logical_dev_id_prefix != logical_dev_id_prefix) {
> > > > +			pr_err("stale registration for PCI domain %d (old prefix 0x%08x, new 0x%08x)\n",
> > > > +			       pci_domain_nr, bus->logical_dev_id_prefix,
> > > > +			       logical_dev_id_prefix);
> > > > +			ret = -EEXIST;
> > > > +		}
> > > > +
> > > > +		goto out_free;
> > > > +	}
> > > > +
> > > > +	new->pci_domain_nr = pci_domain_nr;
> > > > +	new->logical_dev_id_prefix = logical_dev_id_prefix;
> > > > +	list_add(&new->list, &hv_pci_bus_list);
> > > > +	spin_unlock(&hv_pci_bus_lock);
> > > > +	return 0;
> > > > +
> > > > +out_free:
> > > > +	spin_unlock(&hv_pci_bus_lock);
> > > > +	kfree(new);
> > > > +	return ret;
> > > > +}
> > > > +EXPORT_SYMBOL_FOR_MODULES(hv_iommu_register_pci_bus, "pci-hyperv");
> > > > +
> > > > +void hv_iommu_unregister_pci_bus(int pci_domain_nr)
> > > > +{
> > > > +	struct hv_pci_busdata *bus, *tmp;
> > > > +
> > > > +	spin_lock(&hv_pci_bus_lock);
> > > > +	list_for_each_entry_safe(bus, tmp, &hv_pci_bus_list, list) {
> > > > +		if (bus->pci_domain_nr == pci_domain_nr) {
> > > > +			list_del(&bus->list);
> > > > +			kfree(bus);
> > > > +			break;
> > > > +		}
> > > > +	}
> > > > +	spin_unlock(&hv_pci_bus_lock);
> > > > +}
> > > > +EXPORT_SYMBOL_FOR_MODULES(hv_iommu_unregister_pci_bus, "pci-hyperv");
> > > > +
> > > > +/*
> > > > + * Look up the logical device ID prefix registered for @pci_domain_nr.
> > > > + * Returns 0 on success with *prefix filled in; -ENODEV if no entry is
> > > > + * registered for that PCI domain.
> > > > + */
> > > > +int hv_iommu_lookup_logical_dev_id(int pci_domain_nr, u32 *prefix)
> > > > +{
> > > > +	struct hv_pci_busdata *bus;
> > > > +	int ret = -ENODEV;
> > > > +
> > > > +	spin_lock(&hv_pci_bus_lock);
> > > > +	list_for_each_entry(bus, &hv_pci_bus_list, list) {
> > > > +		if (bus->pci_domain_nr == pci_domain_nr) {
> > > > +			*prefix = bus->logical_dev_id_prefix;
> > > > +			ret = 0;
> > > > +			break;
> > > > +		}
> > > > +	}
> > > > +	spin_unlock(&hv_pci_bus_lock);
> > > > +	return ret;
> > > > +}
> > >
> > > I started thinking about the mechanism here because it's somewhat
> > > annoying that it takes 77 lines of code (sans comments) to manage
> > > this simple little mapping. I also started thinking about how many entries
> > > are likely to be in the mapping. A guest VM probably has fewer than 10
> > > entries unless it has multiple NICs and maybe some GPUs. But this code
> > > is also intended to be used by the Linux-as-root code, and I'm thinking
> > > that the number of PCI devices managed by the root could easily be a
> > > hundred or more if the root is managing a couple dozen VMs on a large
> > > physical server. Searching a linked list with 100 or more entries could be
> > > a bit slow.
> > >
> > > If only the guest scenario were needed, you could declare a static
> > > array with 64 entries (64 is an arbitrary upper bound), and just search
> > > through the array instead of having to allocate memory, deal with
> > > allocation failures, and deal with linked lists. But a fixed size array
> > > would need to be much bigger for the root scenario, and you would
> > > still be doing a linear search.
> > >
> > > A better alternative to consider is rhashtable, which is an existing
> > > Linux kernel facility. Based on what an AI bot generated for me, the
> > > code for setting up and using rhashtable is straightforward, and
> > > would probably result in far fewer than 77 lines of code. Lookups
> > > would also be faster than a linear search, at least for the root case
> > > with more than just a few entries. I'd suggest looking at rhashtable
> > > to see whether you like how the resulting code comes out for this
> > > use case, and whether it really is simpler than a roll-your-own linked
> > > list.
> > >
> > 
> > Thank you so much for thinking this through, Michael! That is really
> > a valid concern.
> > 
> > How about using XArray? It might be more lightweight compared with
> > rhashtable. Using pci_domain_nr as the key and prefix as the value.
> > Maybe sth. like:
> >                                                                                                                                                         ┃
> > 	static DEFINE_XARRAY(hv_pci_bus_xa);
> > 
> > 	int hv_iommu_register_pci_bus(int domain_nr, u32 prefix)
> > 	{
> > 		return xa_insert(&hv_pci_bus_xa, domain_nr,
> > 				xa_mk_value(prefix), GFP_KERNEL);
> > 	}                                                                                                                                                ┃
> > 
> > 	void hv_iommu_unregister_pci_bus(int domain_nr)
> > 	{
> > 		xa_erase(&hv_pci_bus_xa, domain_nr);
> > 	}
> > 
> > 	int hv_iommu_lookup_logical_dev_id(int domain_nr, u32 *prefix)
> > 	{
> > 		void *entry = xa_load(&hv_pci_bus_xa, domain_nr);
> > 		if (!entry)
> > ┃
> > 			return -ENODEV;
> > 		*prefix = xa_to_value(entry);
> > 		return 0;
> > ┃
> > 	}
> > 
> 
> xarray is best where the keys are dense or mostly dense integers. In
> this use case, the pci_domain_nr keys are essentially random 16-bit
> values, which doesn't fit xarray as well. It would work, but wouldn't
> be very efficient. See the 2nd paragraph of the documentation here:
> 
> https://www.kernel.org/doc/html/latest/core-api/xarray.html#xarray
> 

Thanks for pointing out the documentation. You're right. With sparse
16-bit keys, a hash-based data structure makes more sense than a
radix-based one. I'll try reworking it with rhashtable and let you
know if it doesn't work out.

B.R.
Yu
> Michael

^ permalink raw reply

* Re: [PATCH net-next v11 2/2] net: mana: force full-page RX buffers via ethtool private flag
From: Dipayaan Roy @ 2026-07-09 15:48 UTC (permalink / raw)
  To: Simon Horman, Paolo Abeni
  Cc: kys, haiyangz, wei.liu, decui, andrew+netdev, davem, edumazet,
	kuba, pabeni, leon, longli, kotaranov, shradhagupta, ssengar,
	ernis, shirazsaleem, linux-hyperv, netdev, linux-kernel,
	linux-rdma, stephen, jacob.e.keller, dipayanroy, leitao, kees,
	john.fastabend, hawk, bpf, daniel, ast, sdf, yury.norov,
	pavan.chebbi
In-Reply-To: <20260708155741.1509815-2-horms@kernel.org>

On Wed, Jul 08, 2026 at 04:57:42PM +0100, Simon Horman wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> Full review at: https://netdev-ai.bots.linux.dev/sashiko/
> ---
> net: mana: force full-page RX buffers via ethtool private flag
> 
> This adds an ethtool private flag "full-page-rx" that forces one RX buffer
> per page to avoid a page_pool fragment path throughput regression on some
> 4K PAGE_SIZE ARM64 platforms. Toggling the flag while the port is up
> triggers a mana_detach() + mana_attach() reconfiguration to apply the new
> buffer layout.
> 
> A few questions inline.
> 
> > diff --git a/drivers/net/ethernet/microsoft/mana/mana_en.c b/drivers/net/ethernet/microsoft/mana/mana_en.c
> > index 7438ea6b3f26..38f50b4eef87 100644
> > --- a/drivers/net/ethernet/microsoft/mana/mana_en.c
> > +++ b/drivers/net/ethernet/microsoft/mana/mana_en.c
> 
> [ ... mana_use_single_rxbuf_per_page() and mana_get_rxbuf_cfg() hunks
>       unrelated to the comments below ... ]
> 
> > diff --git a/drivers/net/ethernet/microsoft/mana/mana_ethtool.c b/drivers/net/ethernet/microsoft/mana/mana_ethtool.c
> > index 3531abfdaf43..24f6ce63b031 100644
> > --- a/drivers/net/ethernet/microsoft/mana/mana_ethtool.c
> > +++ b/drivers/net/ethernet/microsoft/mana/mana_ethtool.c
> 
> [ ... ]
> 
> > @@ -611,6 +630,88 @@ static int mana_get_link_ksettings(struct net_device *ndev,
> >  	return 0;
> >  }
> >  
> > +static u32 mana_get_priv_flags(struct net_device *ndev)
> > +{
> > +	struct mana_port_context *apc = netdev_priv(ndev);
> > +
> > +	return apc->priv_flags;
> > +}
> > +
> > +static int mana_set_priv_flags(struct net_device *ndev, u32 priv_flags)
> > +{
> > +	struct mana_port_context *apc = netdev_priv(ndev);
> > +	u32 changed = apc->priv_flags ^ priv_flags;
> > +	u32 old_priv_flags = apc->priv_flags;
> > +	bool schedule_port_reset = false;
> > +	int err = 0;
> > +
> > +	if (!changed)
> > +		return 0;
> > +
> > +	/* Reject unknown bits */
> > +	if (priv_flags & ~GENMASK(MANA_PRIV_FLAG_MAX - 1, 0))
> > +		return -EINVAL;
> > +
> > +	if (changed & BIT(MANA_PRIV_FLAG_USE_FULL_PAGE_RXBUF)) {
> > +		apc->priv_flags = priv_flags;
> 
> [Low]
> Is the storage of priv_flags intentionally nested inside the per-flag
> branch?  With only one flag today this is fine, but when a second bit is
> added to enum mana_priv_flag_bits, a user toggling only the new bit
> passes the !changed and unknown-bits checks, the outer if is skipped,
> apc->priv_flags is never updated, and mana_set_priv_flags() returns 0
> while mana_get_priv_flags() keeps reporting the old state.
> 
> Would it be cleaner to store priv_flags unconditionally after validation
> and use per-flag if (changed & BIT(...)) blocks only for the
> flag-specific reconfiguration?
> 

Hi Simon, Paolo,

Agreed on this future concern,
but also we donot plan to have another mana driver private flag.


> > +
> > +		if (!apc->port_is_up) {
> > +			/* Port is down, flag updated to apply on next up
> > +			 * so just return.
> > +			 */
> > +			return 0;
> > +		}
> > +
> > +		/* Pre-allocate buffers to prevent failure in mana_attach
> > +		 * later
> > +		 */
> > +		err = mana_pre_alloc_rxbufs(apc, ndev->mtu, apc->num_queues);
> 
> [Low]
> When the MTU is already jumbo (mtu + MANA_RXBUF_PAD > PAGE_SIZE / 2) or
> XDP is attached, mana_use_single_rxbuf_per_page() already returns true
> regardless of the new priv flag:
> 
> 	if (apc->priv_flags & BIT(MANA_PRIV_FLAG_USE_FULL_PAGE_RXBUF))
> 		return true;
> 
> 	/* For xdp and jumbo frames make sure only one packet fits per page. */
> 	if (mtu + MANA_RXBUF_PAD > PAGE_SIZE / 2 || mana_xdp_get(apc))
> 		return true;
> 
> In that case mana_get_rxbuf_cfg() produces identical output whether the
> new flag is on or off, yet toggling the flag here always performs a full
> mana_pre_alloc_rxbufs() + mana_detach() + mana_attach() cycle and flaps
> the link.
> 
> Would a pre/post comparison of mana_get_rxbuf_cfg() output be worth
> adding to skip the reconfiguration when nothing actually changes?
Good point will add a pre and post comparison as well to skip the
detach/attach when already in that configuration.

> 
> [High]
> mana_set_priv_flags() drives the same detach/attach sequence used by
> mana_set_channels() and mana_per_port_queue_reset_work_handler(), but
> does not take apc->vport_mutex and set apc->channel_changing = true
> across the window.
> 
> The struct comment on channel_changing spells out the invariant:
> 
> 	/* Set by mana_set_channels() under vport_mutex to block RDMA
> 	 * from grabbing the vport during the detach/attach window.
> 	 * Checked by mana_cfg_vport() when called from the RDMA path.
> 	 */
> 	bool channel_changing;
> 
> Without it, after mana_detach() uncfg's the vport an RDMA client can
> call mana_cfg_vport() and bump vport_use_count, and then mana_attach()
> will fail with -EBUSY when it re-cfg's the vport.
> 
> There is also no equivalent of the '!port_is_up && vport_use_count'
> early -EBUSY check that mana_set_channels() performs, so is the flag
> allowed to be changed silently while an RDMA client is using the vport?
> 
Agreed. Will add the same vport_mutex + channel_changing pattern used by
mana_set_channels() and mana_per_port_queue_reset_work_handler() to
block RDMA from grabbing the vport during the detach/attach window.

> > +		if (err) {
> > +			netdev_err(ndev,
> > +				   "Insufficient memory for new allocations\n");
> > +			apc->priv_flags = old_priv_flags;
> > +			return err;
> > +		}
> > +
> > +		err = mana_detach(ndev, false);
> 
> [High]
> mana_detach() begins with ASSERT_RTNL(), and so does mana_attach()
> called below.  Is RTNL guaranteed to be held on the SET priv-flags path?
> 
> mana registers net_shaper_ops in mana_devops, which makes it an
> ops-locked driver, so netdev_need_ops_lock() is true and the ethtool
> core acquires rtnl only when the driver opts in via
> ETHTOOL_OP_NEEDS_RTNL_* for that specific command.
> 
> Looking at mana_ethtool_ops.op_needs_rtnl:
> 
> 	.op_needs_rtnl		= ETHTOOL_OP_NEEDS_RTNL_SCHANNELS |
> 				  ETHTOOL_OP_NEEDS_RTNL_SRINGPARAM |
> 				  ETHTOOL_OP_NEEDS_RTNL_GLINK,
> 
> ETHTOOL_OP_NEEDS_RTNL_SPFLAGS is not listed, so ASSERT_RTNL() in the
> detach/attach helpers below will WARN, and the code races with
> RTNL-serialized mutators (mana_open, mana_close, mana_change_mtu,
> mana_tx_timeout) that read and write apc->port_is_up, apc->rxqs,
> apc->tx_qp and apc->port_st_save.
> 
> Should ETHTOOL_OP_NEEDS_RTNL_SPFLAGS be added to op_needs_rtnl, matching
> what is already done for SET channels?
> 
Agreed, Will add ETHTOOL_OP_NEEDS_RTNL_SPFLAGS to .op_needs_rtnl.
> > +		if (err) {
> > +			netdev_err(ndev, "mana_detach failed: %d\n", err);
> > +			apc->priv_flags = old_priv_flags;
> > +
> > +			/* Port is in an inconsistent state. Restore
> > +			 * 'port_is_up' so that queue reset work handler
> > +			 * can properly detach and re-attach.
> > +			 */
> > +			apc->port_is_up = true;
> > +			schedule_port_reset = true;
> > +			goto out;
> > +		}
> > +
> > +		err = mana_attach(ndev);
> > +		if (err) {
> > +			netdev_err(ndev, "mana_attach failed: %d\n", err);
> > +			apc->priv_flags = old_priv_flags;
> > +
> > +			/* Restore 'port_is_up' so the reset work handler
> > +			 * can properly detach/attach. Without this,
> > +			 * the handler sees port_is_up=false and skips
> > +			 * queue allocation, leaving the port dead.
> > +			 */
> > +			apc->port_is_up = true;
> > +			schedule_port_reset = true;
> 
> [High]
> On this branch, mana_detach() has already succeeded and run
> mana_cleanup_port_context(), so apc->rxqs is NULL and queues are torn
> down.  Setting apc->port_is_up = true here between now and when
> queue_reset_work runs opens a window where callers gate on port_is_up
> as their "safe to access rxqs/tx_qp" predicate.
> 
> For example, mana_get_ethtool_stats() does:
> 
> 	if (!apc->port_is_up)
> 		return;
> 	...
> 	rxq = apc->rxqs[q];
> 	...
> 
> A concurrent ethtool -S invocation during that window will pass the
> port_is_up gate and dereference apc->rxqs[q]->stats on a NULL rxqs.
> 
> Is the port_is_up restore actually needed for the reset work handler's
> mana_detach() call?  On the reset path, mana_detach() takes its early
> return when !netif_device_present(ndev):
> 
> 	if (!from_close && !netif_device_present(ndev))
> 		return 0;
> 
> That early return does not touch apc->port_st_save, so the saved state
> from the earlier successful mana_detach() should already be intact for
> the follow-up mana_attach().
> 
> There is also no smp_wmb() paired with this write, unlike the pattern
> used inside mana_detach()/mana_attach() proper.
> 

For the mana_attach() failure path: agreed, will remove port_is_up =
true. After a successful mana_detach(ndev, false), netif_device_detach()
has already been called so the reset handler's mana_detach() takes the
early return via !netif_device_present(), preserving port_st_save for
the subsequent mana_attach().

For the mana_detach() failure path: keeping port_is_up = true is
intentional. Here netif_device_detach() was never reached, so the reset
handler's mana_detach() would re-enter fully and overwrite port_st_save
from port_is_up, if left as false, mana_attach() would skip queue
allocation and leave the port dead.


> > +		}
> > +	}
> > +
> > +out:
> > +	mana_pre_dealloc_rxbufs(apc);
> > +
> > +	if (schedule_port_reset)
> > +		queue_work(apc->ac->per_port_queue_reset_wq,
> > +			   &apc->queue_reset_work);
> > +
> > +	return err;
> > +}
> > +
> 
> [ ... remaining hunks unrelated to the comments above ... ]

Thanks for the review comments, I will address them in the next
version.


Regards
Dipayaan Roy

^ permalink raw reply

* Re: [PATCH] mshv: fix fd leak in mshv_ioctl_create_vtl()
From: Markus Elfring @ 2026-07-09 16:05 UTC (permalink / raw)
  To: Yi Xie, linux-hyperv, Haiyang Zhang, Hamza Mahfooz,
	K. Y. Srinivasan, Wei Liu
  Cc: LKML
In-Reply-To: <20260708012852.36824-1-xieyi@kylinos.cn>

> put_unused_fd() if anon_inode_getfile() fails.

How do you think about to add any tags (like “Fixes” and “Cc”) accordingly?

See also:
* https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/submitting-patches.rst?h=v7.2-rc2#n145
* https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/stable-kernel-rules.rst?h=v7.2-rc2#n34



* Would you like to complete the exception handling by using another goto chain?

* How do you think about to increase the application of scope-based resource management?


Regards,
Markus

^ permalink raw reply

* RE: [PATCH v2 1/4] hyperv: Introduce new hypercall interfaces used by Hyper-V guest IOMMU
From: Michael Kelley @ 2026-07-09 19:06 UTC (permalink / raw)
  To: Yu Zhang, linux-kernel@vger.kernel.org,
	linux-hyperv@vger.kernel.org, iommu@lists.linux.dev,
	linux-pci@vger.kernel.org, linux-arch@vger.kernel.org
  Cc: wei.liu@kernel.org, kys@microsoft.com, haiyangz@microsoft.com,
	decui@microsoft.com, longli@microsoft.com, joro@8bytes.org,
	will@kernel.org, robin.murphy@arm.com, bhelgaas@google.com,
	kwilczynski@kernel.org, lpieralisi@kernel.org, mani@kernel.org,
	robh@kernel.org, arnd@arndb.de, jgg@ziepe.ca, Michael Kelley,
	jacob.pan@linux.microsoft.com, tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <20260702160518.311234-2-zhangyu1@linux.microsoft.com>

From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> 
> From: Wei Liu <wei.liu@kernel.org>
> 
> Hyper-V guest IOMMU is a para-virtualized IOMMU based on hypercalls.
> Introduce the hypercalls used by the child partition to interact with
> this facility.
> 
> These hypercalls fall into below categories:
> - Detection and capability: HVCALL_GET_IOMMU_CAPABILITIES is used to
>   detect the existence and capabilities of the guest IOMMU.
> 
> - Device management: HVCALL_GET_LOGICAL_DEVICE_PROPERTY is used to
>   check whether an endpoint device is managed by the guest IOMMU.
> 
> - Domain management: A set of hypercalls is provided to handle the
>   creation, configuration, and deletion of guest domains, as well as
>   the attachment/detachment of endpoint devices to/from those domains.
> 
> - IOTLB flushing: HVCALL_FLUSH_DEVICE_DOMAIN is used to ask Hyper-V
>   for a domain-selective IOTLB flush (which in its handler may flush
>   the device TLB as well).
> 
> Signed-off-by: Wei Liu <wei.liu@kernel.org>
> Co-developed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> Co-developed-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> ---
>  include/hyperv/hvgdk_mini.h |   8 +++
>  include/hyperv/hvhdk_mini.h | 124 ++++++++++++++++++++++++++++++++++++
>  2 files changed, 132 insertions(+)
> 
> diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
> index 6a4e8b9d570f..5bdbb44da112 100644
> --- a/include/hyperv/hvgdk_mini.h
> +++ b/include/hyperv/hvgdk_mini.h
> @@ -486,10 +486,16 @@ union hv_vp_assist_msr_contents {	 /*
> HV_REGISTER_VP_ASSIST_PAGE */
>  #define HVCALL_GET_VP_INDEX_FROM_APIC_ID		0x009a
>  #define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_SPACE	0x00af
>  #define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_LIST	0x00b0
> +#define HVCALL_CREATE_DEVICE_DOMAIN			0x00b1
> +#define HVCALL_ATTACH_DEVICE_DOMAIN			0x00b2
>  #define HVCALL_SIGNAL_EVENT_DIRECT			0x00c0
>  #define HVCALL_POST_MESSAGE_DIRECT			0x00c1
>  #define HVCALL_DISPATCH_VP				0x00c2
> +#define HVCALL_DETACH_DEVICE_DOMAIN			0x00c4
> +#define HVCALL_DELETE_DEVICE_DOMAIN			0x00c5
>  #define HVCALL_GET_GPA_PAGES_ACCESS_STATES		0x00c9
> +#define HVCALL_CONFIGURE_DEVICE_DOMAIN			0x00ce
> +#define HVCALL_FLUSH_DEVICE_DOMAIN			0x00d0
>  #define HVCALL_ACQUIRE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d7
>  #define HVCALL_RELEASE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d8
>  #define HVCALL_MODIFY_SPARSE_GPA_PAGE_HOST_VISIBILITY	0x00db
> @@ -502,6 +508,8 @@ union hv_vp_assist_msr_contents {	 /*
> HV_REGISTER_VP_ASSIST_PAGE */
>  #define HVCALL_MMIO_READ				0x0106
>  #define HVCALL_MMIO_WRITE				0x0107
>  #define HVCALL_DISABLE_HYP_EX                           0x010f
> +#define HVCALL_GET_IOMMU_CAPABILITIES			0x0125
> +#define HVCALL_GET_LOGICAL_DEVICE_PROPERTY		0x0127
>  #define HVCALL_MAP_STATS_PAGE2				0x0131
> 
>  /* HV_HYPERCALL_INPUT */
> diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
> index b4cb2fa26e9b..493608e791b4 100644
> --- a/include/hyperv/hvhdk_mini.h
> +++ b/include/hyperv/hvhdk_mini.h
> @@ -547,4 +547,128 @@ union hv_device_id {		/* HV_DEVICE_ID */
>  	} acpi;
>  } __packed;
> 
> +/* Device domain types */
> +#define HV_DEVICE_DOMAIN_TYPE_S1	1 /* Stage 1 domain */
> +
> +/* ID for default domain and NULL domain */
> +#define HV_DEVICE_DOMAIN_ID_DEFAULT 0
> +#define HV_DEVICE_DOMAIN_ID_NULL    0xFFFFFFFFULL
> +
> +union hv_device_domain_id {
> +	u64 as_uint64;
> +	struct {
> +		u32 type: 4;
> +		u32 reserved: 28;
> +		u32 id;
> +	} __packed;
> +};
> +
> +struct hv_input_device_domain {
> +	u64 partition_id;
> +	union hv_input_vtl owner_vtl;
> +	u8 padding[7];
> +	union hv_device_domain_id domain_id;
> +} __packed;
> +
> +union hv_create_device_domain_flags {
> +	u32 as_uint32;
> +	struct {
> +		u32 forward_progress_required: 1;
> +		u32 inherit_owning_vtl: 1;
> +		u32 reserved: 30;
> +	} __packed;
> +};
> +
> +struct hv_input_create_device_domain {
> +	struct hv_input_device_domain device_domain;
> +	union hv_create_device_domain_flags create_device_domain_flags;
> +} __packed;
> +
> +struct hv_input_delete_device_domain {
> +	struct hv_input_device_domain device_domain;
> +} __packed;
> +
> +struct hv_input_attach_device_domain {
> +	struct hv_input_device_domain device_domain;
> +	union hv_device_id device_id;
> +} __packed;
> +
> +struct hv_input_detach_device_domain {
> +	u64 partition_id;
> +	union hv_device_id device_id;
> +} __packed;
> +
> +struct hv_device_domain_settings {
> +	struct {
> +		/*
> +		 * Enable translations. If not enabled, all transaction bypass
> +		 * S1 translations.
> +		 */
> +		u64 translation_enabled: 1;
> +		u64 blocked: 1;
> +		/*
> +		 * First stage address translation paging mode:
> +		 * 0: 4-level paging (default)
> +		 * 1: 5-level paging
> +		 */
> +		u64 first_stage_paging_mode: 1;
> +		u64 reserved: 61;
> +	} flags;
> +
> +	/* Address of translation table */
> +	u64 page_table_root;
> +} __packed;
> +
> +struct hv_input_configure_device_domain {
> +	struct hv_input_device_domain device_domain;
> +	struct hv_device_domain_settings settings;
> +} __packed;
> +
> +struct hv_input_get_iommu_capabilities {
> +	u64 partition_id;
> +	u64 reserved;
> +} __packed;
> +
> +struct hv_output_get_iommu_capabilities {
> +	u32 size;
> +	u16 reserved;
> +	u8  max_iova_width;
> +	u8  max_pasid_width;
> +
> +#define HV_IOMMU_CAP_PRESENT (1ULL << 0)
> +#define HV_IOMMU_CAP_S2 (1ULL << 1)
> +#define HV_IOMMU_CAP_S1 (1ULL << 2)
> +#define HV_IOMMU_CAP_S1_5LVL (1ULL << 3)
> +#define HV_IOMMU_CAP_PASID (1ULL << 4)
> +#define HV_IOMMU_CAP_ATS (1ULL << 5)
> +#define HV_IOMMU_CAP_PRI (1ULL << 6)

hvgdk_mini.h mostly uses the Linux BIT() and BIT_ULL()
macros instead of explicit shifts. But this is hvhdk_mini.h.
Does it need to play by a different set of rules for
compatibility with the original Windows files? I think
the Linux BIT* macros are preferable if possible. 

> +
> +	u64 iommu_cap;
> +	u64 pgsize_bitmap;
> +} __packed;
> +
> +enum hv_logical_device_property_code {
> +	HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU = 10,
> +};
> +
> +struct hv_input_get_logical_device_property {
> +	u64 partition_id;
> +	u64 logical_device_id;
> +	/* Takes values from enum hv_logical_device_property_code. */
> +	u32 code;
> +	u32 reserved;
> +} __packed;
> +
> +struct hv_output_get_logical_device_property {
> +#define HV_DEVICE_IOMMU_ENABLED (1ULL << 0)

Same here about BIT_ULL().

> +	u64 device_iommu;
> +	u64 reserved;
> +} __packed;
> +
> +struct hv_input_flush_device_domain {
> +	struct hv_input_device_domain device_domain;
> +	u32 flags;
> +	u32 reserved;
> +} __packed;
> +
>  #endif /* _HV_HVHDK_MINI_H */
> --
> 2.52.0
> 


^ permalink raw reply

* RE: [PATCH v2 3/4] iommu/hyperv: Add para-virtualized IOMMU support for Hyper-V guest
From: Michael Kelley @ 2026-07-09 19:08 UTC (permalink / raw)
  To: Yu Zhang, linux-kernel@vger.kernel.org,
	linux-hyperv@vger.kernel.org, iommu@lists.linux.dev,
	linux-pci@vger.kernel.org, linux-arch@vger.kernel.org
  Cc: wei.liu@kernel.org, kys@microsoft.com, haiyangz@microsoft.com,
	decui@microsoft.com, longli@microsoft.com, joro@8bytes.org,
	will@kernel.org, robin.murphy@arm.com, bhelgaas@google.com,
	kwilczynski@kernel.org, lpieralisi@kernel.org, mani@kernel.org,
	robh@kernel.org, arnd@arndb.de, jgg@ziepe.ca, Michael Kelley,
	jacob.pan@linux.microsoft.com, tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <20260702160518.311234-4-zhangyu1@linux.microsoft.com>

From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> 
> Add a para-virtualized IOMMU driver for Linux guests running on Hyper-V.
> This driver implements stage-1 IO translation within the guest OS.
> It integrates with the Linux IOMMU core, utilizing Hyper-V hypercalls
> for:
>  - Capability discovery
>  - Domain allocation, configuration, and deallocation
>  - Device attachment and detachment
>  - IOTLB invalidation
> 
> The driver constructs x86-compatible stage-1 IO page tables in the
> guest memory using consolidated IO page table helpers. This allows
> the guest to manage stage-1 translations independently of vendor-
> specific drivers (like Intel VT-d or AMD IOMMU).
> 
> Hyper-V consumes this stage-1 IO page table when a device domain is
> created and configured, and nests it with the host's stage-2 IO page
> tables, therefore eliminating the VM exits for guest IOMMU mapping
> operations. For unmapping operations, VM exits to perform the IOTLB
> flush are still unavoidable.
> 
> To identify a device in its hypercall interface, the driver looks up the
> logical device ID prefix registered for the device's PCI domain (see the
> logical device ID registry in hv_common.c) and combines it with the PCI
> function number of the endpoint device.
> 
> Co-developed-by: Wei Liu <wei.liu@kernel.org>
> Signed-off-by: Wei Liu <wei.liu@kernel.org>
> Co-developed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> ---
>  arch/x86/hyperv/hv_init.c       |   4 +
>  arch/x86/include/asm/mshyperv.h |   4 +
>  drivers/iommu/Kconfig           |   1 +
>  drivers/iommu/hyperv/Kconfig    |  16 +
>  drivers/iommu/hyperv/Makefile   |   1 +
>  drivers/iommu/hyperv/iommu.c    | 620 ++++++++++++++++++++++++++++++++
>  drivers/iommu/hyperv/iommu.h    |  51 +++
>  7 files changed, 697 insertions(+)
>  create mode 100644 drivers/iommu/hyperv/Kconfig
>  create mode 100644 drivers/iommu/hyperv/iommu.c
>  create mode 100644 drivers/iommu/hyperv/iommu.h
> 
> diff --git a/arch/x86/hyperv/hv_init.c b/arch/x86/hyperv/hv_init.c
> index 55a8b6de2865..094f9f7ddb72 100644
> --- a/arch/x86/hyperv/hv_init.c
> +++ b/arch/x86/hyperv/hv_init.c
> @@ -578,6 +578,10 @@ void __init hyperv_init(void)
>  	old_setup_percpu_clockev = x86_init.timers.setup_percpu_clockev;
>  	x86_init.timers.setup_percpu_clockev = hv_stimer_setup_percpu_clockev;
> 
> +#ifdef CONFIG_HYPERV_PVIOMMU
> +	x86_init.iommu.iommu_init = hv_iommu_init;
> +#endif
> +

This approach to .iommu_init is a bit different from the Intel VT-d and
AMD IOMMU initialization. Those cases detect the existence of the
IOMMU first via a "detect" function that is called in pci_iommu_alloc().
If the detect function finds an IOMMU, it sets .iommu_init. Any
reason not to use the same approach for the Hyper-V pvIOMMU?
One problem with exactly the same approach is that Hyper-V
hypercalls aren't set up at the time pci_iommu_alloc() runs.
So you'd have to call the "detect" function here in hyperv_init(),
and have the detect function set .iommu_init if pvIOMMU
support is present.

While the code currently in this patch works, it generates boot
time errors if the kernel is built with CONFIG_HYPERV_PVIOMMU
but run in a guest on a host without pvIOMMU support:

[    0.101673] Hyper-V pvIOMMU: HVCALL_GET_IOMMU_CAPABILITIES failed, status 2
[    0.101675] Hyper-V pvIOMMU: HVCALL_GET_IOMMU_CAPABILITIES failed: -22

We really don't want errors if it's just the case that there's no
pvIOMMU support. A less alarming message (at INFO level instead
of ERROR level) about running without an IOMMU might be OK, but
perhaps is unnecessary since you have an INFO message if the
pvIOMMU is found and successfully initialized.

>  	hv_apic_init();
> 
>  	x86_init.pci.arch_init = hv_pci_init;
> diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h
> index f64393e853ee..20d947c2c758 100644
> --- a/arch/x86/include/asm/mshyperv.h
> +++ b/arch/x86/include/asm/mshyperv.h
> @@ -313,6 +313,10 @@ static inline void mshv_vtl_return_hypercall(void) {}
>  static inline void __mshv_vtl_return_call(struct mshv_vtl_cpu_context *vtl0) {}
>  #endif
> 
> +#ifdef CONFIG_HYPERV_PVIOMMU
> +int __init hv_iommu_init(void);
> +#endif
> +
>  #include <asm-generic/mshyperv.h>
> 
>  #endif
> diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig
> index 6e07bd69467a..0d128f377929 100644
> --- a/drivers/iommu/Kconfig
> +++ b/drivers/iommu/Kconfig
> @@ -195,6 +195,7 @@ config MSM_IOMMU
>  source "drivers/iommu/amd/Kconfig"
>  source "drivers/iommu/arm/Kconfig"
>  source "drivers/iommu/intel/Kconfig"
> +source "drivers/iommu/hyperv/Kconfig"
>  source "drivers/iommu/iommufd/Kconfig"
>  source "drivers/iommu/riscv/Kconfig"
> 
> diff --git a/drivers/iommu/hyperv/Kconfig b/drivers/iommu/hyperv/Kconfig
> new file mode 100644
> index 000000000000..8b6abbaaf9b8
> --- /dev/null
> +++ b/drivers/iommu/hyperv/Kconfig
> @@ -0,0 +1,16 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +# HyperV paravirtualized IOMMU support
> +config HYPERV_PVIOMMU
> +	bool "Microsoft Hypervisor para-virtualized IOMMU support"
> +	depends on X86_64 && HYPERV
> +	select IOMMU_API
> +	select GENERIC_PT
> +	select IOMMU_PT
> +	select IOMMU_PT_X86_64
> +	select IOMMU_IOVA
> +	default HYPERV
> +	help
> +	  Para-virtualized IOMMU driver for Linux guests running on
> +	  Microsoft Hyper-V. Provides DMA remapping and IOTLB
> +	  flush support to enable DMA isolation for devices

I think this is specifically "PCI devices", right?  VMBus devices
that do DMA (storvsc and netvsc) don't use the pvIOMMU.
The "assigned to the guest" phrase pretty much implies "PCI",
but it would be clearer to be explicit.

> +	  assigned to the guest.
> diff --git a/drivers/iommu/hyperv/Makefile b/drivers/iommu/hyperv/Makefile
> index 6ef0ef97f3dd..fefb409d976b 100644
> --- a/drivers/iommu/hyperv/Makefile
> +++ b/drivers/iommu/hyperv/Makefile
> @@ -1,2 +1,3 @@
>  # SPDX-License-Identifier: GPL-2.0
>  obj-$(CONFIG_IRQ_REMAP) += hv-irq-remap-x86.o
> +obj-$(CONFIG_HYPERV_PVIOMMU) += iommu.o
> diff --git a/drivers/iommu/hyperv/iommu.c b/drivers/iommu/hyperv/iommu.c
> new file mode 100644
> index 000000000000..254136946404
> --- /dev/null
> +++ b/drivers/iommu/hyperv/iommu.c
> @@ -0,0 +1,620 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +/*
> + * Hyper-V IOMMU driver.
> + *
> + * Copyright (C) 2019, 2024-2026 Microsoft, Inc.
> + */
> +
> +#define pr_fmt(fmt) "Hyper-V pvIOMMU: " fmt
> +#define dev_fmt(fmt) pr_fmt(fmt)
> +
> +#include <linux/iommu.h>
> +#include <linux/pci.h>
> +#include <linux/dma-map-ops.h>
> +#include <linux/generic_pt/iommu.h>
> +#include <linux/pci-ats.h>
> +
> +#include <asm/iommu.h>
> +#include <asm/hypervisor.h>
> +#include <asm/mshyperv.h>
> +
> +#include "iommu.h"
> +#include "../iommu-pages.h"
> +
> +struct hv_iommu_dev *hv_iommu_device;
> +
> +/*
> + * Identity and blocking domains are static singletons: identity is a 1:1
> + * passthrough with no page table, blocking rejects all DMA. Neither holds
> + * per-IOMMU state, so one instance suffices even with multiple vIOMMUs.
> + */
> +static const struct iommu_domain_ops hv_iommu_identity_domain_ops;
> +static const struct iommu_domain_ops hv_iommu_blocking_domain_ops;
> +static struct iommu_ops hv_iommu_ops;
> +
> +static struct hv_iommu_domain hv_identity_domain = {
> +	.domain = {
> +		.type	= IOMMU_DOMAIN_IDENTITY,
> +		.ops	= &hv_iommu_identity_domain_ops,
> +		.owner	= &hv_iommu_ops,
> +	},
> +};
> +static struct hv_iommu_domain hv_blocking_domain = {
> +	.domain = {
> +		.type	= IOMMU_DOMAIN_BLOCKED,
> +		.ops	= &hv_iommu_blocking_domain_ops,
> +		.owner	= &hv_iommu_ops,
> +	},
> +};
> +
> +static inline bool hv_iommu_present(u64 cap)
> +{
> +	return cap & HV_IOMMU_CAP_PRESENT;
> +}
> +
> +static inline bool hv_iommu_s1_domain_supported(u64 cap)
> +{
> +	return cap & HV_IOMMU_CAP_S1;
> +}
> +
> +static inline bool hv_iommu_5lvl_supported(u64 cap)
> +{
> +	return cap & HV_IOMMU_CAP_S1_5LVL;
> +}
> +
> +static inline bool hv_iommu_ats_supported(u64 cap)
> +{
> +	return cap & HV_IOMMU_CAP_ATS;
> +}
> +
> +static int hv_create_device_domain(struct hv_iommu_domain *hv_domain, u32 domain_stage)
> +{
> +	int ret;
> +	u64 status;
> +	unsigned long flags;
> +	struct hv_input_create_device_domain *input;
> +
> +	ret = ida_alloc_range(&hv_iommu_device->domain_ids,
> +			hv_iommu_device->first_domain, hv_iommu_device->last_domain,
> +			GFP_KERNEL);
> +	if (ret < 0)
> +		return ret;
> +
> +	hv_domain->device_domain.partition_id = HV_PARTITION_ID_SELF;
> +	hv_domain->device_domain.domain_id.type = domain_stage;
> +	hv_domain->device_domain.domain_id.id = ret;
> +	hv_domain->hv_iommu = hv_iommu_device;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +	input->device_domain = hv_domain->device_domain;
> +	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)) {
> +		pr_err("HVCALL_CREATE_DEVICE_DOMAIN failed, status %lld\n", status);
> +		ida_free(&hv_iommu_device->domain_ids, hv_domain->device_domain.domain_id.id);
> +	}
> +
> +	return hv_result_to_errno(status);
> +}
> +
> +static void hv_delete_device_domain(struct hv_iommu_domain *hv_domain)
> +{
> +	u64 status;
> +	unsigned long flags;
> +	struct hv_input_delete_device_domain *input;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +	input->device_domain = hv_domain->device_domain;
> +	status = hv_do_hypercall(HVCALL_DELETE_DEVICE_DOMAIN, input, NULL);
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_DELETE_DEVICE_DOMAIN failed, status %lld\n", status);
> +
> +	ida_free(&hv_domain->hv_iommu->domain_ids, hv_domain->device_domain.domain_id.id);
> +}
> +
> +static bool hv_iommu_capable(struct device *dev, enum iommu_cap cap)
> +{
> +	switch (cap) {
> +	case IOMMU_CAP_CACHE_COHERENCY:
> +		return true;
> +	case IOMMU_CAP_DEFERRED_FLUSH:
> +		return true;
> +	default:
> +		return false;
> +	}
> +}
> +
> +static void hv_flush_device_domain(struct hv_iommu_domain *hv_domain)
> +{
> +	u64 status;
> +	unsigned long flags;
> +	struct hv_input_flush_device_domain *input;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +	input->device_domain = hv_domain->device_domain;
> +	status = hv_do_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN, input, NULL);
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_FLUSH_DEVICE_DOMAIN failed, status %lld\n", status);
> +}
> +
> +static int hv_iommu_attach_dev(struct iommu_domain *domain, struct device *dev,
> +			       struct iommu_domain *old)
> +{
> +	u64 status;
> +	u32 prefix;
> +	unsigned long flags;
> +	struct pci_dev *pdev;
> +	struct hv_input_attach_device_domain *input;
> +	struct hv_iommu_endpoint *vdev = dev_iommu_priv_get(dev);
> +	struct hv_iommu_domain *hv_domain = to_hv_iommu_domain(domain);
> +	int ret;
> +
> +	if (vdev->hv_domain == hv_domain)
> +		return 0;
> +
> +	pdev = to_pci_dev(dev);
> +	dev_dbg(dev, "attaching to domain %d\n",
> +		hv_domain->device_domain.domain_id.id);
> +
> +	ret = hv_iommu_lookup_logical_dev_id(pci_domain_nr(pdev->bus), &prefix);
> +	if (ret) {
> +		dev_err(&pdev->dev, "no IOMMU registration for vPCI bus\n");
> +		return ret;
> +	}
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +	input->device_domain = hv_domain->device_domain;
> +	input->device_id.as_uint64 = (u64)prefix | PCI_FUNC(pdev->devfn);
> +	status = hv_do_hypercall(HVCALL_ATTACH_DEVICE_DOMAIN, input, NULL);
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_ATTACH_DEVICE_DOMAIN failed, status %lld\n", status);
> +	else
> +		vdev->hv_domain = hv_domain;
> +
> +	return hv_result_to_errno(status);
> +}
> +
> +static int hv_iommu_blocking_attach_dev(struct iommu_domain *domain,
> +					struct device *dev,
> +					struct iommu_domain *old)
> +{
> +	int ret = hv_iommu_attach_dev(domain, dev, old);
> +
> +	/*
> +	 * Attaching to the blocking domain only asks the hypervisor to
> +	 * disable translation and IOPF for the device, so it cannot fail
> +	 * unless there is a driver or hypervisor bug. Return the hypercall
> +	 * status rather than 0 so that a failure on the DMA ownership claim
> +	 * path (VFIO/iommufd) fails the claim instead of leaving the device
> +	 * unblocked. WARN since such a failure indicates a bug.
> +	 */
> +	WARN_ON(ret);
> +	return ret;
> +}
> +
> +static int hv_iommu_get_logical_device_property(struct device *dev,
> +					u32 code,
> +					struct hv_output_get_logical_device_property *property)
> +{
> +	u64 status;
> +	u32 prefix;
> +	unsigned long flags;
> +	int ret;
> +	struct pci_dev *pdev = to_pci_dev(dev);
> +	struct hv_input_get_logical_device_property *input;
> +	struct hv_output_get_logical_device_property *output;
> +
> +	ret = hv_iommu_lookup_logical_dev_id(pci_domain_nr(pdev->bus), &prefix);
> +	if (ret)
> +		return ret;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	output = (struct hv_output_get_logical_device_property *)(input + 1);
> +	memset(input, 0, sizeof(*input));
> +	input->partition_id = HV_PARTITION_ID_SELF;
> +	input->logical_device_id = (u64)prefix | PCI_FUNC(pdev->devfn);
> +	input->code = code;
> +	status = hv_do_hypercall(HVCALL_GET_LOGICAL_DEVICE_PROPERTY, input, output);
> +	*property = *output;
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_GET_LOGICAL_DEVICE_PROPERTY failed, status %lld\n", status);
> +
> +	return hv_result_to_errno(status);
> +}
> +
> +static struct iommu_device *hv_iommu_probe_device(struct device *dev)
> +{
> +	struct pci_dev *pdev;
> +	struct hv_iommu_endpoint *vdev;
> +	struct hv_output_get_logical_device_property device_iommu_property = {0};
> +
> +	if (!dev_is_pci(dev))
> +		return ERR_PTR(-ENODEV);
> +
> +	pdev = to_pci_dev(dev);
> +
> +	if (hv_iommu_get_logical_device_property(dev,
> +						 HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU,
> +						 &device_iommu_property) ||
> +	    !(device_iommu_property.device_iommu & HV_DEVICE_IOMMU_ENABLED))
> +		return ERR_PTR(-ENODEV);
> +
> +	vdev = kzalloc_obj(*vdev, GFP_KERNEL);
> +	if (!vdev)
> +		return ERR_PTR(-ENOMEM);
> +
> +	vdev->dev = dev;
> +	vdev->hv_iommu = hv_iommu_device;
> +	dev_iommu_priv_set(dev, vdev);
> +
> +	if (hv_iommu_ats_supported(hv_iommu_device->cap) &&
> +	    pci_ats_supported(pdev))
> +		pci_enable_ats(pdev, __ffs(hv_iommu_device->pgsize_bitmap));
> +
> +	return &vdev->hv_iommu->iommu;
> +}
> +
> +static void hv_iommu_release_device(struct device *dev)
> +{
> +	struct hv_iommu_endpoint *vdev = dev_iommu_priv_get(dev);
> +	struct pci_dev *pdev = to_pci_dev(dev);
> +
> +	if (pdev->ats_enabled)
> +		pci_disable_ats(pdev);
> +
> +	dev_iommu_priv_set(dev, NULL);
> +
> +	kfree(vdev);
> +}
> +
> +static struct iommu_group *hv_iommu_device_group(struct device *dev)
> +{
> +	if (dev_is_pci(dev))
> +		return pci_device_group(dev);
> +
> +	WARN_ON_ONCE(1);
> +	return generic_device_group(dev);
> +}
> +
> +static int hv_configure_device_domain(struct hv_iommu_domain *hv_domain, u32 domain_type)
> +{
> +	u64 status;
> +	unsigned long flags;
> +	struct pt_iommu_x86_64_hw_info pt_info;
> +	struct hv_input_configure_device_domain *input;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +	input->device_domain = hv_domain->device_domain;
> +	input->settings.flags.blocked = (domain_type == IOMMU_DOMAIN_BLOCKED);
> +	/*
> +	 * Clearing translation_enabled bypasses translation (DMA uses the GPA
> +	 * directly), which only suits identity. The hypervisor requires paging
> +	 * and blocked domains to keep it set.
> +	 */
> +	input->settings.flags.translation_enabled = (domain_type != IOMMU_DOMAIN_IDENTITY);
> +
> +	if (domain_type & __IOMMU_DOMAIN_PAGING) {
> +		pt_iommu_x86_64_hw_info(&hv_domain->pt_iommu_x86_64, &pt_info);
> +		input->settings.page_table_root = pt_info.gcr3_pt;
> +		input->settings.flags.first_stage_paging_mode =
> +			pt_info.levels == 5;
> +	}
> +	status = hv_do_hypercall(HVCALL_CONFIGURE_DEVICE_DOMAIN, input, NULL);
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_CONFIGURE_DEVICE_DOMAIN failed, status %lld\n", status);
> +
> +	return hv_result_to_errno(status);
> +}
> +
> +static int __init hv_initialize_static_domains(void)
> +{
> +	int ret;
> +	struct hv_iommu_domain *hv_domain;
> +
> +	/* Default stage-1 identity domain */
> +	hv_domain = &hv_identity_domain;
> +
> +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> +	if (ret)
> +		return ret;
> +
> +	ret = hv_configure_device_domain(hv_domain, IOMMU_DOMAIN_IDENTITY);
> +	if (ret)
> +		goto delete_identity_domain;
> +
> +	/* Default stage-1 blocked domain */
> +	hv_domain = &hv_blocking_domain;
> +
> +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> +	if (ret)
> +		goto delete_identity_domain;
> +
> +	ret = hv_configure_device_domain(hv_domain, IOMMU_DOMAIN_BLOCKED);
> +	if (ret)
> +		goto delete_blocked_domain;
> +
> +	return 0;
> +
> +delete_blocked_domain:
> +	hv_delete_device_domain(&hv_blocking_domain);
> +delete_identity_domain:
> +	hv_delete_device_domain(&hv_identity_domain);
> +	return ret;
> +}
> +
> +/* x86 architectural MSI address range */
> +#define INTERRUPT_RANGE_START	(0xfee00000)
> +#define INTERRUPT_RANGE_END	(0xfeefffff)

These same constants are also defined in the Intel and AMD
IOMMU drivers. Bonus points for creating a common definition
in a .h file that can be shared by all the drivers. :-)
 
> +static void hv_iommu_get_resv_regions(struct device *dev,
> +		struct list_head *head)
> +{
> +	struct iommu_resv_region *region;
> +
> +	region = iommu_alloc_resv_region(INTERRUPT_RANGE_START,
> +				      INTERRUPT_RANGE_END - INTERRUPT_RANGE_START + 1,
> +				      0, IOMMU_RESV_MSI, GFP_KERNEL);
> +	if (!region)
> +		return;
> +
> +	list_add_tail(&region->list, head);
> +}
> +
> +static void hv_iommu_flush_iotlb_all(struct iommu_domain *domain)
> +{
> +	hv_flush_device_domain(to_hv_iommu_domain(domain));
> +}
> +
> +static void hv_iommu_iotlb_sync(struct iommu_domain *domain,
> +				struct iommu_iotlb_gather *iotlb_gather)
> +{
> +	hv_flush_device_domain(to_hv_iommu_domain(domain));
> +
> +	iommu_put_pages_list(&iotlb_gather->freelist);
> +}
> +
> +static void hv_iommu_paging_domain_free(struct iommu_domain *domain)
> +{
> +	struct hv_iommu_domain *hv_domain = to_hv_iommu_domain(domain);
> +
> +	/* Free all remaining mappings */
> +	pt_iommu_deinit(&hv_domain->pt_iommu);
> +
> +	hv_delete_device_domain(hv_domain);
> +
> +	kfree(hv_domain);
> +}
> +
> +static const struct iommu_domain_ops hv_iommu_identity_domain_ops = {
> +	.attach_dev	= hv_iommu_attach_dev,
> +};
> +
> +static const struct iommu_domain_ops hv_iommu_blocking_domain_ops = {
> +	.attach_dev	= hv_iommu_blocking_attach_dev,
> +};
> +
> +static const struct iommu_domain_ops hv_iommu_paging_domain_ops = {
> +	.attach_dev	= hv_iommu_attach_dev,
> +	IOMMU_PT_DOMAIN_OPS(x86_64),
> +	.flush_iotlb_all = hv_iommu_flush_iotlb_all,
> +	.iotlb_sync = hv_iommu_iotlb_sync,
> +	.free = hv_iommu_paging_domain_free,
> +};
> +
> +static struct iommu_domain *hv_iommu_domain_alloc_paging(struct device *dev)
> +{
> +	int ret;
> +	struct hv_iommu_domain *hv_domain;
> +	struct pt_iommu_x86_64_cfg cfg = {};
> +
> +	hv_domain = kzalloc_obj(*hv_domain, GFP_KERNEL);
> +	if (!hv_domain)
> +		return ERR_PTR(-ENOMEM);
> +
> +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> +	if (ret)
> +		goto err_free;
> +
> +	hv_domain->pt_iommu.nid = dev_to_node(dev);
> +
> +	cfg.common.hw_max_vasz_lg2 = hv_iommu_device->max_iova_width;
> +	cfg.common.hw_max_oasz_lg2 = 52;
> +	cfg.top_level = (hv_iommu_device->max_iova_width > 48) ? 4 : 3;
> +
> +	ret = pt_iommu_x86_64_init(&hv_domain->pt_iommu_x86_64, &cfg, GFP_KERNEL);
> +	if (ret)
> +		goto err_delete_domain;
> +
> +	/* Constrain to page sizes the hypervisor supports */
> +	hv_domain->domain.pgsize_bitmap &= hv_iommu_device->pgsize_bitmap;
> +
> +	hv_domain->domain.ops = &hv_iommu_paging_domain_ops;
> +
> +	ret = hv_configure_device_domain(hv_domain, __IOMMU_DOMAIN_PAGING);
> +	if (ret)
> +		goto err_pt_deinit;
> +
> +	return &hv_domain->domain;
> +
> +err_pt_deinit:
> +	pt_iommu_deinit(&hv_domain->pt_iommu);
> +err_delete_domain:
> +	hv_delete_device_domain(hv_domain);
> +err_free:
> +	kfree(hv_domain);
> +	return ERR_PTR(ret);
> +}
> +
> +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,
> +	.release_device		  = hv_iommu_release_device,
> +	.device_group		  = hv_iommu_device_group,
> +	.get_resv_regions	  = hv_iommu_get_resv_regions,
> +	.owner			  = THIS_MODULE,
> +	.identity_domain	  = &hv_identity_domain.domain,
> +	.blocked_domain		  = &hv_blocking_domain.domain,
> +	.release_domain		  = &hv_blocking_domain.domain,
> +};
> +
> +static int hv_iommu_detect(struct hv_output_get_iommu_capabilities *hv_iommu_cap)
> +{
> +	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 = (struct hv_output_get_iommu_capabilities *)(input + 1);
> +	memset(input, 0, sizeof(*input));
> +	input->partition_id = HV_PARTITION_ID_SELF;
> +	status = hv_do_hypercall(HVCALL_GET_IOMMU_CAPABILITIES, input, output);
> +	*hv_iommu_cap = *output;
> +
> +	local_irq_restore(flags);
> +
> +	if (!hv_result_success(status))
> +		pr_err("HVCALL_GET_IOMMU_CAPABILITIES failed, status %lld\n", status);
> +
> +	return hv_result_to_errno(status);
> +}
> +
> +static void __init hv_init_iommu_device(struct hv_iommu_dev *hv_iommu,
> +			struct hv_output_get_iommu_capabilities *hv_iommu_cap)
> +{
> +	ida_init(&hv_iommu->domain_ids);
> +
> +	hv_iommu->cap = hv_iommu_cap->iommu_cap;
> +	hv_iommu->max_iova_width = hv_iommu_cap->max_iova_width;
> +	if (!hv_iommu_5lvl_supported(hv_iommu->cap) &&
> +	    hv_iommu->max_iova_width > 48) {
> +		pr_info("5-level paging not supported, limiting iova width to 48.\n");
> +		hv_iommu->max_iova_width = 48;
> +	}
> +
> +	hv_iommu->geometry = (struct iommu_domain_geometry) {
> +		.aperture_start = 0,
> +		.aperture_end = (((u64)1) << hv_iommu->max_iova_width) - 1,
> +		.force_aperture = true,
> +	};
> +
> +	hv_iommu->first_domain = HV_DEVICE_DOMAIN_ID_DEFAULT + 1;
> +	hv_iommu->last_domain = HV_DEVICE_DOMAIN_ID_NULL - 1;
> +	hv_iommu->pgsize_bitmap = hv_iommu_cap->pgsize_bitmap;
> +	hv_iommu_device = hv_iommu;
> +}
> +
> +int __init hv_iommu_init(void)
> +{
> +	int ret = 0;
> +	struct hv_iommu_dev *hv_iommu = NULL;
> +	struct hv_output_get_iommu_capabilities hv_iommu_cap = {0};
> +
> +	if (no_iommu || iommu_detected)
> +		return -ENODEV;
> +
> +	if (!hv_is_hyperv_initialized())
> +		return -ENODEV;
> +
> +	ret = hv_iommu_detect(&hv_iommu_cap);
> +	if (ret) {
> +		pr_err("HVCALL_GET_IOMMU_CAPABILITIES failed: %d\n", ret);

hv_iommu_detect() already outputs an error message in the failure case.

> +		return -ENODEV;
> +	}
> +
> +	if (!hv_iommu_present(hv_iommu_cap.iommu_cap) ||
> +	    !hv_iommu_s1_domain_supported(hv_iommu_cap.iommu_cap)) {
> +		pr_err("IOMMU capabilities not sufficient: cap=0x%llx\n",
> +		       hv_iommu_cap.iommu_cap);
> +		return -ENODEV;
> +	}
> +
> +	/*
> +	 * The page table code only maps x86 page sizes (4K/2M/1G); require the
> +	 * hypervisor to advertise a non-empty subset of exactly those.
> +	 */
> +	if (!hv_iommu_cap.pgsize_bitmap ||
> +	    (hv_iommu_cap.pgsize_bitmap & ~(u64)(SZ_4K | SZ_2M | SZ_1G))) {
> +		pr_err("unsupported page sizes: pgsize_bitmap=0x%llx\n",
> +		       hv_iommu_cap.pgsize_bitmap);
> +		return -ENODEV;
> +	}
> +
> +	iommu_detected = 1;
> +	pci_request_acs();
> +
> +	hv_iommu = kzalloc_obj(*hv_iommu, GFP_KERNEL);
> +	if (!hv_iommu)
> +		return -ENOMEM;
> +
> +	hv_init_iommu_device(hv_iommu, &hv_iommu_cap);
> +
> +	ret = hv_initialize_static_domains();
> +	if (ret) {
> +		pr_err("static domains init failed: %d\n", ret);
> +		goto err_free;
> +	}
> +
> +	ret = iommu_device_sysfs_add(&hv_iommu->iommu, NULL, NULL, "%s", "hv-iommu");
> +	if (ret) {
> +		pr_err("iommu_device_sysfs_add failed: %d\n", ret);
> +		goto err_delete_static_domains;
> +	}
> +
> +	ret = iommu_device_register(&hv_iommu->iommu, &hv_iommu_ops, NULL);
> +	if (ret) {
> +		pr_err("iommu_device_register failed: %d\n", ret);
> +		goto err_sysfs_remove;
> +	}
> +
> +	pr_info("successfully initialized\n");
> +	return 0;
> +
> +err_sysfs_remove:
> +	iommu_device_sysfs_remove(&hv_iommu->iommu);
> +err_delete_static_domains:
> +	hv_delete_device_domain(&hv_blocking_domain);
> +	hv_delete_device_domain(&hv_identity_domain);
> +err_free:
> +	kfree(hv_iommu);
> +	return ret;
> +}
> diff --git a/drivers/iommu/hyperv/iommu.h b/drivers/iommu/hyperv/iommu.h
> new file mode 100644
> index 000000000000..3a9f40fa2403
> --- /dev/null
> +++ b/drivers/iommu/hyperv/iommu.h
> @@ -0,0 +1,51 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +
> +/*
> + * Hyper-V IOMMU driver.
> + *
> + * Copyright (C) 2024-2025, Microsoft, Inc.
> + *
> + */
> +
> +#ifndef _HYPERV_IOMMU_H
> +#define _HYPERV_IOMMU_H
> +
> +struct hv_iommu_dev {
> +	struct iommu_device iommu;
> +	struct ida domain_ids;
> +
> +	/* Device configuration */
> +	u8  max_iova_width;
> +	u8  max_pasid_width;
> +	u64 cap;
> +	u64 pgsize_bitmap;
> +
> +	struct iommu_domain_geometry geometry;
> +	u64 first_domain;
> +	u64 last_domain;
> +};
> +
> +struct hv_iommu_domain {
> +	union {
> +		struct iommu_domain    domain;
> +		struct pt_iommu        pt_iommu;
> +		struct pt_iommu_x86_64 pt_iommu_x86_64;
> +	};
> +	struct hv_iommu_dev *hv_iommu;
> +	struct hv_input_device_domain device_domain;
> +	u64		pgsize_bitmap;
> +};
> +
> +PT_IOMMU_CHECK_DOMAIN(struct hv_iommu_domain, pt_iommu, domain);
> +PT_IOMMU_CHECK_DOMAIN(struct hv_iommu_domain, pt_iommu_x86_64.iommu, domain);
> +
> +struct hv_iommu_endpoint {
> +	struct device *dev;
> +	struct hv_iommu_dev *hv_iommu;
> +	struct hv_iommu_domain *hv_domain;
> +};
> +
> +#define to_hv_iommu_domain(d) \
> +	container_of(d, struct hv_iommu_domain, domain)
> +
> +#endif /* _HYPERV_IOMMU_H */
> --
> 2.52.0
> 


^ permalink raw reply

* RE: [PATCH v2 4/4] iommu/hyperv: Add page-selective IOTLB flush support
From: Michael Kelley @ 2026-07-09 19:08 UTC (permalink / raw)
  To: Yu Zhang, linux-kernel@vger.kernel.org,
	linux-hyperv@vger.kernel.org, iommu@lists.linux.dev,
	linux-pci@vger.kernel.org, linux-arch@vger.kernel.org
  Cc: wei.liu@kernel.org, kys@microsoft.com, haiyangz@microsoft.com,
	decui@microsoft.com, longli@microsoft.com, joro@8bytes.org,
	will@kernel.org, robin.murphy@arm.com, bhelgaas@google.com,
	kwilczynski@kernel.org, lpieralisi@kernel.org, mani@kernel.org,
	robh@kernel.org, arnd@arndb.de, jgg@ziepe.ca, Michael Kelley,
	jacob.pan@linux.microsoft.com, tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <20260702160518.311234-5-zhangyu1@linux.microsoft.com>

From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> 
> Add page-selective IOTLB flush using HVCALL_FLUSH_DEVICE_DOMAIN_LIST.
> This hypercall accepts a list of (page_number, page_mask_shift) entries,
> enabling finer-grained IOTLB invalidation compared to the domain-wide
> HVCALL_FLUSH_DEVICE_DOMAIN used by hv_iommu_flush_iotlb_all().
> 
> hv_iommu_calc_flush_range() computes the smallest power-of-two aligned
> range that covers the target IOVA region, producing a single flush
> descriptor. This may over-flush when the range is not naturally aligned,
> matching the approach used by Intel VT-d PSI. If the page-selective
> flush fails, the code falls back to a full domain flush.
> 
> Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> ---
>  drivers/iommu/hyperv/iommu.c | 68 +++++++++++++++++++++++++++++++++++-
>  include/hyperv/hvgdk_mini.h  |  1 +
>  include/hyperv/hvhdk_mini.h  | 17 +++++++++
>  3 files changed, 85 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/iommu/hyperv/iommu.c b/drivers/iommu/hyperv/iommu.c
> index 254136946404..e9b104a322fd 100644
> --- a/drivers/iommu/hyperv/iommu.c
> +++ b/drivers/iommu/hyperv/iommu.c
> @@ -9,6 +9,7 @@
>  #define pr_fmt(fmt) "Hyper-V pvIOMMU: " fmt
>  #define dev_fmt(fmt) pr_fmt(fmt)
> 
> +#include <linux/hyperv.h>
>  #include <linux/iommu.h>
>  #include <linux/pci.h>
>  #include <linux/dma-map-ops.h>
> @@ -401,10 +402,74 @@ static void hv_iommu_flush_iotlb_all(struct iommu_domain *domain)
>  	hv_flush_device_domain(to_hv_iommu_domain(domain));
>  }
> 
> +/*
> + * Calculate the minimal power-of-two aligned range that covers [start, end]
> + * (end is inclusive). Returns a single (page_number, page_mask_shift)
> + * descriptor that may over-flush when the range is not naturally aligned.
> + */
> +static void hv_iommu_calc_flush_range(unsigned long start, unsigned long end,
> +				       union hv_iommu_flush_va *va)
> +{
> +	unsigned long start_pfn = HVPFN_DOWN(start);
> +	unsigned long last_pfn = HVPFN_UP(end + 1) - 1;
> +	unsigned long mask_shift, aligned_pfn;
> +
> +	if (start_pfn == last_pfn) {
> +		mask_shift = 0;
> +	} else {
> +		/*
> +		 * Find the highest bit position where start_pfn and last_pfn
> +		 * differ.  A range aligned to one above that bit is the
> +		 * smallest power-of-two region that covers both endpoints.
> +		 */
> +		mask_shift = __fls(start_pfn ^ last_pfn) + 1;
> +	}
> +
> +	aligned_pfn = ALIGN_DOWN(start_pfn, 1UL << mask_shift);
> +	va->page_number = aligned_pfn;
> +	va->page_mask_shift = mask_shift;
> +}
> +
> +static void hv_flush_device_domain_list(struct hv_iommu_domain *hv_domain,
> +					struct iommu_iotlb_gather *iotlb_gather)
> +{
> +	u64 status;
> +	unsigned long flags;
> +	struct hv_input_flush_device_domain_list *input;
> +
> +	local_irq_save(flags);
> +
> +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> +	memset(input, 0, sizeof(*input));
> +
> +	input->device_domain = hv_domain->device_domain;
> +	input->flags |= HV_FLUSH_DEVICE_DOMAIN_LIST_IOMMU_FORMAT;
> +	hv_iommu_calc_flush_range(iotlb_gather->start, iotlb_gather->end,
> +				  &input->iova_list[0]);
> +
> +	status = hv_do_rep_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN_LIST,
> +				     1, 0, input, NULL);
> +
> +	if (!hv_result_success(status)) {
> +		/* Page-selective flush failed, fall back to full flush. */

With the selective flush now simplified to just a single entry, it really
shouldn't fail, right? Doing a full flush as a fallback makes sense, but
perhaps do a WARN_ON_ONCE() first so that there's an indication that
the selective flush failed.

> +		struct hv_input_flush_device_domain *flush_all = (void *)input;
> +
> +		memset(flush_all, 0, sizeof(*flush_all));
> +		flush_all->device_domain = hv_domain->device_domain;
> +		status = hv_do_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN,
> +					flush_all, NULL);
> +		WARN(!hv_result_success(status),
> +		     "HVCALL_FLUSH_DEVICE_DOMAIN fallback also failed: %lld\n",
> +		     status);
> +	}
> +
> +	local_irq_restore(flags);
> +}
> +
>  static void hv_iommu_iotlb_sync(struct iommu_domain *domain,
>  				struct iommu_iotlb_gather *iotlb_gather)
>  {
> -	hv_flush_device_domain(to_hv_iommu_domain(domain));
> +	hv_flush_device_domain_list(to_hv_iommu_domain(domain), iotlb_gather);
> 
>  	iommu_put_pages_list(&iotlb_gather->freelist);
>  }
> @@ -455,6 +520,7 @@ static struct iommu_domain *hv_iommu_domain_alloc_paging(struct device *dev)
> 
>  	cfg.common.hw_max_vasz_lg2 = hv_iommu_device->max_iova_width;
>  	cfg.common.hw_max_oasz_lg2 = 52;
> +	cfg.common.features |= BIT(PT_FEAT_FLUSH_RANGE);
>  	cfg.top_level = (hv_iommu_device->max_iova_width > 48) ? 4 : 3;
> 
>  	ret = pt_iommu_x86_64_init(&hv_domain->pt_iommu_x86_64, &cfg, GFP_KERNEL);
> diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
> index 5bdbb44da112..eaaf87171478 100644
> --- a/include/hyperv/hvgdk_mini.h
> +++ b/include/hyperv/hvgdk_mini.h
> @@ -496,6 +496,7 @@ union hv_vp_assist_msr_contents {	 /*
> HV_REGISTER_VP_ASSIST_PAGE */
>  #define HVCALL_GET_GPA_PAGES_ACCESS_STATES		0x00c9
>  #define HVCALL_CONFIGURE_DEVICE_DOMAIN			0x00ce
>  #define HVCALL_FLUSH_DEVICE_DOMAIN			0x00d0
> +#define HVCALL_FLUSH_DEVICE_DOMAIN_LIST			0x00d1
>  #define HVCALL_ACQUIRE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d7
>  #define HVCALL_RELEASE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d8
>  #define HVCALL_MODIFY_SPARSE_GPA_PAGE_HOST_VISIBILITY	0x00db
> diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
> index 493608e791b4..f51d5d9467f1 100644
> --- a/include/hyperv/hvhdk_mini.h
> +++ b/include/hyperv/hvhdk_mini.h
> @@ -671,4 +671,21 @@ struct hv_input_flush_device_domain {
>  	u32 reserved;
>  } __packed;
> 
> +union hv_iommu_flush_va {
> +	u64 iova;
> +	struct {
> +		u64 page_mask_shift : 12;
> +		u64 page_number : 52;
> +	};
> +} __packed;
> +
> +
> +struct hv_input_flush_device_domain_list {
> +	struct hv_input_device_domain device_domain;
> +#define HV_FLUSH_DEVICE_DOMAIN_LIST_IOMMU_FORMAT (1 << 0)

Use BIT()?

> +	u32 flags;
> +	u32 reserved;
> +	union hv_iommu_flush_va iova_list[];
> +} __packed;
> +
>  #endif /* _HV_HVHDK_MINI_H */
> --
> 2.52.0
> 


^ permalink raw reply

* Re: [PATCH net-next v3] net: mana: Add handler for sriov configure
From: sashiko-bot @ 2026-07-09 21:01 UTC (permalink / raw)
  To: Haiyang Zhang; +Cc: linux-hyperv
In-Reply-To: <20260708205924.2408673-1-haiyangz@linux.microsoft.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Calling `pci_disable_sriov()` in the PF's `shutdown` path forces the teardown of already-shut-down VFs, triggering a `dev_WARN_ONCE` stack trace that can cause a kernel panic on shutdown.
--

--- Patch [1]: [PATCH net-next v3] net: mana: Add handler for sriov configure ---
commit 4f33a45546e7a470eda04866cd0e6294bcfd526c
Author: Haiyang Zhang <haiyangz@microsoft.com>

net: mana: Add handler for sriov configure

This patch adds an SR-IOV configuration handler to the mana driver, allowing
it to request or disable VFs. It also adds SR-IOV disable calls to the
device remove and shutdown paths.

> diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c b/drivers/net/ethernet/microsoft/mana/gdma_main.c
> index e8b7ffb47eb98..dff66a2a4f598 100644
> --- a/drivers/net/ethernet/microsoft/mana/gdma_main.c
> +++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c
[ ... ]
> @@ -2488,6 +2490,8 @@ static void mana_gd_shutdown(struct pci_dev *pdev)
>  
>  	dev_info(&pdev->dev, "Shutdown was called\n");
>  
> +	pci_disable_sriov(pdev);
> +

[Severity: High]
Will this trigger a kernel warning during system shutdown when VFs are active?

During system shutdown, the driver core calls device_shutdown() iterating in
reverse creation order. First, it calls mana_gd_shutdown() on the VFs, which
partially tears down the VF and disables the PCI device:

mana_gd_shutdown()
    ...
    pci_disable_device(pdev); 

Next, device_shutdown() calls mana_gd_shutdown() on the PF. Calling
pci_disable_sriov() here destroys the VF PCI devices and unregisters them
from the bus, which synchronously forces a call to the VF's remove callback:

mana_gd_remove()
    ...
    pci_disable_device(pdev); 

Since pci_disable_device() is called unconditionally at the end of both
mana_gd_shutdown() and mana_gd_remove(), this second call detects that the
device is already disabled and fires a warning. On production systems with
panic_on_warn=1, this converts a normal shutdown into a kernel panic.

>  	mana_rdma_remove(&gc->mana_ib);
>  	mana_remove(&gc->mana, true);
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260708205924.2408673-1-haiyangz@linux.microsoft.com?part=1

^ permalink raw reply

* [PATCH 0/2] HID: hyperv: bound initial device info descriptor
From: Michael Bommarito @ 2026-07-10  2:28 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, kys, Haiyang Zhang, Wei Liu
  Cc: Dexuan Cui, Long Li, linux-input, linux-hyperv, linux-kernel,
	stable

A malicious Hyper-V host or backend can crash a guest with a short
SYNTH_HID_INITIAL_DEVICE_INFO message. mousevsc_on_receive_device_info()
trusts the HID descriptor bLength and wDescriptorLength without checking
that the received VMBus packet actually contains both byte ranges, so a
truncated packet with an oversized report-descriptor length makes the
guest read past the received packet while copying the descriptor. This
matters most for a confidential guest, where the host is outside the trust
boundary.

Patch 1 passes the received initial-device-info size into the parser and
rejects descriptor lengths that exceed the packet. Patch 2 adds
same-translation-unit KUnit coverage: a well-formed message that must
still parse and the truncated/oversized message that must now be rejected.

Reproduced with the KUnit/KASAN test: stock reads past the packet on the
short message after the benign control passes; patched rejects it and both
cases pass.

Cc: stable@vger.kernel.org

Michael Bommarito (2):
  HID: hyperv: validate initial device info bounds
  HID: hyperv: add KUnit coverage for device info bounds

 drivers/hid/Kconfig      |  10 +++
 drivers/hid/hid-hyperv.c | 144 ++++++++++++++++++++++++++++++++++++---
 2 files changed, 144 insertions(+), 10 deletions(-)

--
2.53.0

^ permalink raw reply

* [PATCH 1/2] HID: hyperv: validate initial device info bounds
From: Michael Bommarito @ 2026-07-10  2:28 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, kys, Haiyang Zhang, Wei Liu
  Cc: Dexuan Cui, Long Li, linux-input, linux-hyperv, linux-kernel,
	stable
In-Reply-To: <20260710022854.3739558-1-michael.bommarito@gmail.com>

The Hyper-V synthetic HID host supplies SYNTH_HID_INITIAL_DEVICE_INFO
messages that contain a HID descriptor followed by the report descriptor
bytes. mousevsc_on_receive_device_info() trusts bLength and
wDescriptorLength without checking that the received packet contains both
byte ranges.

A malformed host or backend message can therefore make the guest read
past the received VMBus packet while copying the report descriptor. Pass
the received initial-device-info size into the parser and reject
descriptor lengths that exceed the packet.

Impact: A malicious Hyper-V host or backend can crash a guest by sending
a short initial device-info message with an oversized HID report
descriptor length.

Fixes: b95f5bcb811e ("HID: Move the hid-hyperv driver out of staging")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5-5-xhigh
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
 drivers/hid/hid-hyperv.c | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-hyperv.c b/drivers/hid/hid-hyperv.c
index 7d2b0063df151..fd90196430e29 100644
--- a/drivers/hid/hid-hyperv.c
+++ b/drivers/hid/hid-hyperv.c
@@ -171,18 +171,32 @@ static void mousevsc_free_device(struct mousevsc_dev *device)
 }
 
 static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
-				struct synthhid_device_info *device_info)
+					    struct synthhid_device_info *device_info,
+					    u32 device_info_size)
 {
 	int ret = 0;
 	struct hid_descriptor *desc;
 	struct mousevsc_prt_msg ack;
+	size_t desc_offset;
+	size_t desc_size;
 
 	input_device->dev_info_status = -ENOMEM;
 
+	if (device_info_size < sizeof(*device_info)) {
+		input_device->dev_info_status = -EINVAL;
+		goto cleanup;
+	}
+
 	input_device->hid_dev_info = device_info->hid_dev_info;
 	desc = &device_info->hid_descriptor;
+	desc_offset = offsetof(struct synthhid_device_info, hid_descriptor);
+	desc_size = device_info_size - desc_offset;
 	if (desc->bLength == 0)
 		goto cleanup;
+	if (desc->bLength < sizeof(*desc) || desc->bLength > desc_size) {
+		input_device->dev_info_status = -EINVAL;
+		goto cleanup;
+	}
 
 	/* The pointer is not NULL when we resume from hibernation */
 	kfree(input_device->hid_desc);
@@ -197,6 +211,10 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
 		input_device->dev_info_status = -EINVAL;
 		goto cleanup;
 	}
+	if (input_device->report_desc_size > desc_size - desc->bLength) {
+		input_device->dev_info_status = -EINVAL;
+		goto cleanup;
+	}
 
 	/* The pointer is not NULL when we resume from hibernation */
 	kfree(input_device->report_desc);
@@ -273,14 +291,17 @@ static void mousevsc_on_receive(struct hv_device *device,
 		break;
 
 	case SYNTH_HID_INITIAL_DEVICE_INFO:
-		WARN_ON(pipe_msg->size < sizeof(struct hv_input_dev_info));
+		if (WARN_ON_ONCE(pipe_msg->size <
+				 sizeof(struct synthhid_device_info)))
+			break;
 
 		/*
 		 * Parse out the device info into device attr,
 		 * hid desc and report desc
 		 */
 		mousevsc_on_receive_device_info(input_dev,
-			(struct synthhid_device_info *)pipe_msg->data);
+						(struct synthhid_device_info *)pipe_msg->data,
+						pipe_msg->size);
 		break;
 	case SYNTH_HID_INPUT_REPORT:
 		input_report =
-- 
2.53.0

^ permalink raw reply related

* [PATCH 2/2] HID: hyperv: add KUnit coverage for device info bounds
From: Michael Bommarito @ 2026-07-10  2:28 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, kys, Haiyang Zhang, Wei Liu
  Cc: Dexuan Cui, Long Li, linux-input, linux-hyperv, linux-kernel,
	stable
In-Reply-To: <20260710022854.3739558-1-michael.bommarito@gmail.com>

Add KUnit coverage for Hyper-V synthetic HID initial device-info parsing.
The tests cover zero bLength, a valid descriptor plus report descriptor,
and a malformed report descriptor length that exceeds the received
message.

The same-translation-unit test uses a KUnit-only ACK bypass so parser
coverage does not require a live VMBus channel.

Assisted-by: Codex:gpt-5-5-xhigh
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
 drivers/hid/Kconfig      |  10 ++++
 drivers/hid/hid-hyperv.c | 117 ++++++++++++++++++++++++++++++++++++---
 2 files changed, 120 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f23..41ca48d9adc9e 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1183,6 +1183,16 @@ config HID_HYPERV_MOUSE
 	help
 	Select this option to enable the Hyper-V mouse driver.
 
+config HID_HYPERV_MOUSE_KUNIT_TEST
+	bool "KUnit tests for Hyper-V mouse driver" if !KUNIT_ALL_TESTS
+	depends on KUNIT && HID_HYPERV_MOUSE
+	default KUNIT_ALL_TESTS
+	help
+	  Builds unit tests for the Hyper-V synthetic HID driver.
+	  These tests exercise the initial device-info parser with
+	  malformed host-provided HID descriptors and are only useful
+	  for kernel developers running KUnit.
+
 config HID_SMARTJOYPLUS
 	tristate "SmartJoy PLUS PS2/USB adapter support"
 	help
diff --git a/drivers/hid/hid-hyperv.c b/drivers/hid/hid-hyperv.c
index fd90196430e29..6579bd19da13a 100644
--- a/drivers/hid/hid-hyperv.c
+++ b/drivers/hid/hid-hyperv.c
@@ -13,6 +13,9 @@
 #include <linux/hiddev.h>
 #include <linux/hyperv.h>
 
+#if IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST)
+#include <kunit/test.h>
+#endif
 
 struct hv_input_dev_info {
 	unsigned int size;
@@ -240,13 +243,18 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
 	ack.ack.header.size = 1;
 	ack.ack.reserved = 0;
 
-	ret = vmbus_sendpacket(input_device->device->channel,
-			&ack,
-			sizeof(struct pipe_prt_msg) +
-			sizeof(struct synthhid_device_info_ack),
-			(unsigned long)&ack,
-			VM_PKT_DATA_INBAND,
-			VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
+	if (IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST) &&
+	    !input_device->device) {
+		ret = 0;
+	} else {
+		ret = vmbus_sendpacket(input_device->device->channel,
+				       &ack,
+				       sizeof(struct pipe_prt_msg) +
+				       sizeof(struct synthhid_device_info_ack),
+				       (unsigned long)&ack,
+				       VM_PKT_DATA_INBAND,
+				       VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
+	}
 
 	if (!ret)
 		input_device->dev_info_status = 0;
@@ -635,5 +643,100 @@ static void __exit mousevsc_exit(void)
 MODULE_LICENSE("GPL");
 MODULE_DESCRIPTION("Microsoft Hyper-V Synthetic HID Driver");
 
+#if IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST)
+static struct mousevsc_dev *mousevsc_kunit_alloc_dev(struct kunit *test)
+{
+	struct mousevsc_dev *input_dev;
+
+	input_dev = kunit_kzalloc(test, sizeof(*input_dev), GFP_KERNEL);
+	if (!input_dev)
+		return NULL;
+
+	init_completion(&input_dev->wait_event);
+
+	return input_dev;
+}
+
+static void mousevsc_device_info_zero_blength(struct kunit *test)
+{
+	struct synthhid_device_info *info;
+	struct mousevsc_dev *input_dev;
+
+	input_dev = mousevsc_kunit_alloc_dev(test);
+	KUNIT_ASSERT_NOT_NULL(test, input_dev);
+	info = kunit_kzalloc(test, sizeof(*info), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, info);
+
+	info->hid_descriptor.bLength = 0;
+
+	mousevsc_on_receive_device_info(input_dev, info, sizeof(*info));
+
+	KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, -ENOMEM);
+}
+
+static void mousevsc_device_info_valid_descriptor(struct kunit *test)
+{
+	struct synthhid_device_info *info;
+	struct mousevsc_dev *input_dev;
+	u8 *report;
+
+	input_dev = mousevsc_kunit_alloc_dev(test);
+	KUNIT_ASSERT_NOT_NULL(test, input_dev);
+	info = kunit_kzalloc(test, sizeof(*info) + 4, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, info);
+
+	info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
+	info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(4);
+	report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
+	memset(report, 0x42, 4);
+
+	mousevsc_on_receive_device_info(input_dev, info, sizeof(*info) + 4);
+
+	KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, 0);
+	KUNIT_EXPECT_EQ(test, input_dev->report_desc_size, 4);
+	KUNIT_EXPECT_MEMEQ(test, input_dev->report_desc, report, 4);
+
+	kfree(input_dev->hid_desc);
+	kfree(input_dev->report_desc);
+}
+
+static void mousevsc_device_info_report_desc_oob(struct kunit *test)
+{
+	struct synthhid_device_info *info;
+	struct mousevsc_dev *input_dev;
+	u8 *report;
+
+	input_dev = mousevsc_kunit_alloc_dev(test);
+	KUNIT_ASSERT_NOT_NULL(test, input_dev);
+	info = kunit_kzalloc(test, sizeof(*info) + 8, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, info);
+
+	info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
+	info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(64);
+	report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
+	memset(report, 0x42, 8);
+
+	mousevsc_on_receive_device_info(input_dev, info, sizeof(*info) + 8);
+
+	KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, -EINVAL);
+
+	kfree(input_dev->hid_desc);
+}
+
+static struct kunit_case mousevsc_test_cases[] = {
+	KUNIT_CASE(mousevsc_device_info_zero_blength),
+	KUNIT_CASE(mousevsc_device_info_valid_descriptor),
+	KUNIT_CASE(mousevsc_device_info_report_desc_oob),
+	{}
+};
+
+static struct kunit_suite mousevsc_test_suite = {
+	.name = "hid_hyperv_mouse",
+	.test_cases = mousevsc_test_cases,
+};
+
+kunit_test_suite(mousevsc_test_suite);
+#endif
+
 module_init(mousevsc_init);
 module_exit(mousevsc_exit);
-- 
2.53.0

^ permalink raw reply related

* [PATCH] hv: hv_balloon: validate unballoon range count
From: Michael Bommarito @ 2026-07-10  2:29 UTC (permalink / raw)
  To: kys, Haiyang Zhang, Wei Liu, Dexuan Cui, Long Li
  Cc: Greg Kroah-Hartman, linux-hyperv, linux-kernel, stable

The Hyper-V dynamic memory host supplies DM_UNBALLOON_REQUEST messages
with a header size and a range_count field. balloon_down() trusts
range_count and walks req->range_array without checking that the received
message contains that many ranges.

A malformed host or backend message can therefore make the guest read
past the received VMBus packet while freeing balloon ranges. Validate the
received message size and reject range_count values that exceed the
present range array before walking it.

Impact: A malicious Hyper-V host or backend can crash a guest by sending
a short unballoon request with an oversized range_count.

Fixes: 9aa8b50b2b3d ("Drivers: hv: Add Hyper-V balloon driver")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5-5-xhigh
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
 drivers/hv/hv_balloon.c | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)

diff --git a/drivers/hv/hv_balloon.c b/drivers/hv/hv_balloon.c
index a848400a59a2d..f5bc8c9fea7b9 100644
--- a/drivers/hv/hv_balloon.c
+++ b/drivers/hv/hv_balloon.c
@@ -1337,8 +1337,23 @@ static void balloon_up(struct work_struct *dummy)
 	}
 }
 
+static bool unballoon_request_valid(struct dm_unballoon_request *req,
+				    u32 msg_size)
+{
+	u32 max_ranges;
+
+	if (msg_size < sizeof(*req) || req->hdr.size < sizeof(*req) ||
+	    req->hdr.size > msg_size)
+		return false;
+
+	max_ranges = (req->hdr.size - sizeof(*req)) /
+		     sizeof(req->range_array[0]);
+
+	return req->range_count <= max_ranges;
+}
+
 static void balloon_down(struct hv_dynmem_device *dm,
-			 struct dm_unballoon_request *req)
+			 struct dm_unballoon_request *req, u32 msg_size)
 {
 	union dm_mem_page_range *range_array = req->range_array;
 	int range_count = req->range_count;
@@ -1346,6 +1361,12 @@ static void balloon_down(struct hv_dynmem_device *dm,
 	int i;
 	unsigned int prev_pages_ballooned = dm->num_pages_ballooned;
 
+	if (!unballoon_request_valid(req, msg_size)) {
+		pr_warn_ratelimited("Invalid unballoon request: size %u, header size %u, range count %u\n",
+				    msg_size, req->hdr.size, req->range_count);
+		return;
+	}
+
 	for (i = 0; i < range_count; i++) {
 		free_balloon_pages(dm, &range_array[i]);
 		complete(&dm_device.config_event);
@@ -1527,7 +1548,8 @@ static void balloon_onchannelcallback(void *context)
 
 			dm->state = DM_BALLOON_DOWN;
 			balloon_down(dm,
-				     (struct dm_unballoon_request *)recv_buffer);
+				     (struct dm_unballoon_request *)recv_buffer,
+				     recvlen);
 			break;
 
 		case DM_MEM_HOT_ADD_REQUEST:
-- 
2.53.0

^ permalink raw reply related

* Re: [PATCH 1/2] HID: hyperv: validate initial device info bounds
From: sashiko-bot @ 2026-07-10  2:40 UTC (permalink / raw)
  To: Michael Bommarito; +Cc: linux-hyperv, dmitry.torokhov, linux-input
In-Reply-To: <20260710022854.3739558-2-michael.bommarito@gmail.com>

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:

New issues:
- [High] Incomplete Vulnerability Fix / Out-of-Bounds Read
- [Medium] Missing Completion / Timeout Hang
- [Low] Dead Code / Redundant Check

Pre-existing issues:
- [High] Race Condition / Use-After-Free
- [High] Out-of-Bounds Access (Read/Write) in Probe Workaround
- [High] Out-of-Bounds Read (Information Leak) in Input Report
--

--- Patch [1]: [PATCH 1/2] HID: hyperv: validate initial device info bounds ---
commit a1876077d0b8322cccf9c09f43d240367749b037
Author: Michael Bommarito <michael.bommarito@gmail.com>
Subject: HID: hyperv: validate initial device info bounds

This commit attempts to fix an out-of-bounds read by passing the received
initial-device-info size into the parser and rejecting descriptor lengths
that exceed the packet.

> diff --git a/drivers/hid/hid-hyperv.c b/drivers/hid/hid-hyperv.c
> --- a/drivers/hid/hid-hyperv.c
> +++ b/drivers/hid/hid-hyperv.c
> @@ -171,18 +171,32 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
> -				struct synthhid_device_info *device_info)
> +					    struct synthhid_device_info *device_info,
> +					    u32 device_info_size)
>  {
>  	int ret = 0;
>  	struct hid_descriptor *desc;
>  	struct mousevsc_prt_msg ack;
> +	size_t desc_offset;
> +	size_t desc_size;
>  
>  	input_device->dev_info_status = -ENOMEM;
>  
> +	if (device_info_size < sizeof(*device_info)) {
> +		input_device->dev_info_status = -EINVAL;
> +		goto cleanup;
> +	}

[Severity: Low]
Is this check necessary? In mousevsc_on_receive(), before calling this
function, the code explicitly verifies pipe_msg->size >=
sizeof(struct synthhid_device_info). Since device_info_size is passed
directly from pipe_msg->size, this inner check appears to be unreachable.

> +
>  	input_device->hid_dev_info = device_info->hid_dev_info;
>  	desc = &device_info->hid_descriptor;
> +	desc_offset = offsetof(struct synthhid_device_info, hid_descriptor);
> +	desc_size = device_info_size - desc_offset;
>  	if (desc->bLength == 0)
>  		goto cleanup;
> +	if (desc->bLength < sizeof(*desc) || desc->bLength > desc_size) {
> +		input_device->dev_info_status = -EINVAL;
> +		goto cleanup;
> +	}
>  
>  	/* The pointer is not NULL when we resume from hibernation */
>  	kfree(input_device->hid_desc);
> @@ -197,6 +211,10 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
>  		input_device->dev_info_status = -EINVAL;
>  		goto cleanup;
>  	}
> +	if (input_device->report_desc_size > desc_size - desc->bLength) {
> +		input_device->dev_info_status = -EINVAL;
> +		goto cleanup;
> +	}
>  
>  	/* The pointer is not NULL when we resume from hibernation */
>  	kfree(input_device->report_desc);

[Severity: High]
This is a pre-existing issue, but can this lead to a use-after-free?

During mousevsc_probe(), the driver waits for the first
SYNTH_HID_INITIAL_DEVICE_INFO and proceeds to hid_add_device(), which parses
input_dev->report_desc.

If the host concurrently sends a second SYNTH_HID_INITIAL_DEVICE_INFO packet,
the channel callback processes it asynchronously and calls
mousevsc_on_receive_device_info(). This executes
kfree(input_device->report_desc) without any locking while the HID core is
actively reading it.

[ ... ]
> @@ -273,14 +291,17 @@ static void mousevsc_on_receive(struct hv_device *device,
>  		break;
>  
>  	case SYNTH_HID_INITIAL_DEVICE_INFO:
> -		WARN_ON(pipe_msg->size < sizeof(struct hv_input_dev_info));
> +		if (WARN_ON_ONCE(pipe_msg->size <
> +				 sizeof(struct synthhid_device_info)))
> +			break;

[Severity: Medium]
If this break is taken, are we skipping the completion event?

By breaking out of the switch statement early, we skip the call to
mousevsc_on_receive_device_info(). As a result, the
complete(&input_device->wait_event) inside its cleanup path is never
executed. This might cause the waiting thread in mousevsc_connect_to_vsp()
to stall until the 5-second timeout expires.

>  
>  		/*
>  		 * Parse out the device info into device attr,
>  		 * hid desc and report desc
>  		 */
>  		mousevsc_on_receive_device_info(input_dev,
> -			(struct synthhid_device_info *)pipe_msg->data);
> +						(struct synthhid_device_info *)pipe_msg->data,
> +						pipe_msg->size);

[Severity: High]
Does this fully prevent out-of-bounds reads if the host sends a malicious
payload?

The pipe_msg->size field comes from the untrusted payload. If a malicious
host sends a short physical VMBus packet but sets pipe_msg->size to an
artificially large value, the bounds checks in
mousevsc_on_receive_device_info() will pass because they are evaluated
against this large device_info_size.

Could we restrict the size to the actual VMBus packet boundary, perhaps using
something like (packet->len8 - packet->offset8) << 3, to ensure we don't
read past the received packet?

>  		break;
>  	case SYNTH_HID_INPUT_REPORT:
>  		input_report =
> 			(struct synthhid_input_report *)pipe_msg->data;
> 		if (!input_dev->init_complete)
> 			break;
> 
> 		len = min(input_report->header.size,
> 			  (u32)sizeof(input_dev->input_buf));
> 		memcpy(input_dev->input_buf, input_report->buffer, len);

[Severity: High]
This is a pre-existing issue, but does this blind trust of
input_report->header.size enable an out-of-bounds read?

When handling SYNTH_HID_INPUT_REPORT, a malicious host could send a
physically short packet but set input_report->header.size to 4096.

The memcpy length is bounded using:

len = min(input_report->header.size, (u32)sizeof(input_dev->input_buf));

However, it isn't validated against the actual VMBus packet envelope. This
could result in copying beyond the packet boundary, potentially causing an
access violation or exposing memory adjacent to the packet.


[Severity: High]
This is a pre-existing issue, but I noticed another potential out-of-bounds
access during the probe workaround in mousevsc_probe():

drivers/hid/hid-hyperv.c:mousevsc_probe() {
    ...
    if (input_dev->report_desc[14] == 0x25)
        input_dev->report_desc[14] = 0x29;
    ...
}

If a malicious host provides a wDescriptorLength of less than 15 bytes
(for instance, 8 bytes), this accesses and potentially writes to memory past
the dynamically allocated report_desc buffer. Should we ensure the buffer is
at least 15 bytes long before accessing index 14?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260710022854.3739558-1-michael.bommarito@gmail.com?part=1

^ permalink raw reply

* Re: [PATCH 2/2] HID: hyperv: add KUnit coverage for device info bounds
From: sashiko-bot @ 2026-07-10  2:41 UTC (permalink / raw)
  To: Michael Bommarito; +Cc: linux-hyperv, linux-input, dmitry.torokhov
In-Reply-To: <20260710022854.3739558-3-michael.bommarito@gmail.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:

New issues:
- [Low] The embedded KUnit test will be silently disabled and cannot be compiled when both KUnit and the Hyper-V mouse driver are configured as modules.
- [Low] Test-specific mock logic is injected directly into the production VMBus I/O path.

Pre-existing issues:
- [High] The driver and the newly added bounds checking rely on unvalidated inner payload sizes (`pipe_msg->size`, `input_report->header.size`) instead of validating against the actual physical VMBus transport packet size, exposing the guest to OOB reads.
- [High] Missing bounds check before accessing `report_desc[14]` allows out-of-bounds heap memory corruption.
--

--- Patch [2]: [PATCH 2/2] HID: hyperv: add KUnit coverage for device info bounds ---
commit d676a1b2eb911b6f9e0db4e282e431e24c17d123
Author: Michael Bommarito <michael.bommarito@gmail.com>
Subject: HID: hyperv: add KUnit coverage for device info bounds

This commit adds KUnit tests for the Hyper-V synthetic HID initial
device-info parser. The tests exercise malformed host-provided HID
descriptors using a KUnit-only ACK bypass.

> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -1215,6 +1215,16 @@ config HID_HYPERV_MOUSE
[ ... ]
> +config HID_HYPERV_MOUSE_KUNIT_TEST
> +	bool "KUnit tests for Hyper-V mouse driver" if !KUNIT_ALL_TESTS
> +	depends on KUNIT && HID_HYPERV_MOUSE
[Severity: Low]
When both KUNIT and HID_HYPERV_MOUSE are configured as modules, will this
regression cause the dependency expression to evaluate to "m", forcing the
boolean config to "n" and silently preventing the test from being enabled for
module builds?

The standard kernel idiom for this is "depends on KUNIT=y || KUNIT=HID_HYPERV_MOUSE".

> +	default KUNIT_ALL_TESTS
[ ... ]
> diff --git a/drivers/hid/hid-hyperv.c b/drivers/hid/hid-hyperv.c
> --- a/drivers/hid/hid-hyperv.c
> +++ b/drivers/hid/hid-hyperv.c
[ ... ]
> @@ -240,13 +243,18 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
[ ... ]
> -	ret = vmbus_sendpacket(input_device->device->channel,
> -			&ack,
> -			sizeof(struct pipe_prt_msg) +
> -			sizeof(struct synthhid_device_info_ack),
> -			(unsigned long)&ack,
> -			VM_PKT_DATA_INBAND,
> -			VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
> +	if (IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST) &&
> +	    !input_device->device) {
[Severity: Low]
Is this regression injecting test-specific mock logic directly into the
production VMBus I/O path?

Could we instead extract the device-info validation into a pure helper
function that the KUnit tests can call directly, avoiding the need to
conditionally bypass network sends in the core logic?

> +		ret = 0;
> +	} else {
> +		ret = vmbus_sendpacket(input_device->device->channel,
[ ... ]
> @@ -635,5 +643,100 @@ static void __exit mousevsc_exit(void)
[ ... ]
> +static void mousevsc_device_info_valid_descriptor(struct kunit *test)
> +{
[ ... ]
> +	info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
> +	info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(4);
[Severity: High]
This is a pre-existing issue, but does this test highlight an incomplete
bounds check in the driver?

The test sets the report descriptor length to 4 bytes. However, looking at
mousevsc_probe(), the driver contains a workaround that accesses index 14:

mousevsc_probe() {
    ...
    /* workaround SA-167 */
    if (input_dev->report_desc[14] == 0x25)
        input_dev->report_desc[14] = 0x29;
    ...
}

If a compromised host provides a valid descriptor with a length between 1 and
14, as tested here, could this result in an out-of-bounds heap memory read
and write since the bounds check in mousevsc_on_receive_device_info() only
ensures the size is greater than zero?

> +	report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
[ ... ]
> +static void mousevsc_device_info_report_desc_oob(struct kunit *test)
> +{
[ ... ]
> +	info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
> +	info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(64);
> +	report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
> +	memset(report, 0x42, 8);
> +
> +	mousevsc_on_receive_device_info(input_dev, info, sizeof(*info) + 8);
[Severity: High]
This is a pre-existing issue, but does the bounds validation tested here
rely on an unvalidated inner payload size rather than the physical VMBus
transport packet size?

If a compromised or malicious Hyper-V host sends a VMBus packet with a
massive pipe_msg->size but a small physical payload:

mousevsc_on_receive() {
    ...
    mousevsc_on_receive_device_info(input_dev,
                                    (struct synthhid_device_info *)pipe_msg->data,
                                    pipe_msg->size);
    ...
}

Could mousevsc_on_receive() pass this spoofable size to
mousevsc_on_receive_device_info(), leading to out-of-bounds reads during
memcpy() or kmemdup() because the bounds checking trusts this inner size?

> +
> +	KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, -EINVAL);
> +
> +	kfree(input_dev->hid_desc);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260710022854.3739558-1-michael.bommarito@gmail.com?part=2

^ permalink raw reply

* Re: [PATCH] hv: hv_balloon: validate unballoon range count
From: sashiko-bot @ 2026-07-10  2:42 UTC (permalink / raw)
  To: Michael Bommarito; +Cc: linux-hyperv
In-Reply-To: <20260710022914.3740453-1-michael.bommarito@gmail.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

Pre-existing issues:
- [Critical] free_balloon_pages() blindly trusts untrusted host-provided PFNs, causing arbitrary memory free.
- [High] Teardown race in balloon_remove() leads to Use-After-Free.
- [High] Data race when updating worker arguments from VMBus softirq callbacks.
- [High] Concurrent lockless modification of dm->num_pages_ballooned.
- [High] Pending workqueues are leaked on the balloon_probe() error path.
--

--- Patch [1]: [PATCH] hv: hv_balloon: validate unballoon range count ---
commit cffe2d5855cc693f743d4cc5594c147a6fff1bdb
Author: Michael Bommarito <michael.bommarito@gmail.com>

hv: hv_balloon: validate unballoon range count

The Hyper-V dynamic memory host supplies DM_UNBALLOON_REQUEST messages
with a header size and a range_count field. balloon_down() trusts
range_count and walks req->range_array without checking that the received
message contains that many ranges. A malformed host or backend message can
therefore make the guest read past the received VMBus packet while freeing
balloon ranges.

> diff --git a/drivers/hv/hv_balloon.c b/drivers/hv/hv_balloon.c
> index 42ce27be344d5..0ebfc2524d630 100644
> --- a/drivers/hv/hv_balloon.c
> +++ b/drivers/hv/hv_balloon.c

[ ... ]

> @@ -1346,6 +1361,12 @@ static void balloon_down(struct hv_dynmem_device *dm,
>  	int i;
>  	unsigned int prev_pages_ballooned = dm->num_pages_ballooned;

[Severity: High]
This is a pre-existing issue, but is it safe to modify
dm->num_pages_ballooned without locking or atomic operations?

If a DM_UNBALLOON_REQUEST arrives while a DM_BALLOON_REQUEST is still being
processed, free_balloon_pages() executes synchronously in the VMBus tasklet
context and decrements the counter:

drivers/hv/hv_balloon.c:free_balloon_pages() {
	...
		dm->num_pages_ballooned--;
	...
}

At the same time, alloc_balloon_pages() executes asynchronously in the
workqueue and increments it:

drivers/hv/hv_balloon.c:alloc_balloon_pages() {
	...
		dm->num_pages_ballooned += alloc_unit;
	...
}

Could lost updates corrupt the guest's memory pressure statistics, leading
to incorrect accounting when reported back to the host via
get_pages_committed()?

>  
> +	if (!unballoon_request_valid(req, msg_size)) {
> +		pr_warn_ratelimited("Invalid unballoon request: size %u, header size %u, range count %u\n",
> +				    msg_size, req->hdr.size, req->range_count);
> +		return;
> +	}
> +
>  	for (i = 0; i < range_count; i++) {
>  		free_balloon_pages(dm, &range_array[i]);
>  		complete(&dm_device.config_event);

[Severity: Critical]
This is a pre-existing issue, but does free_balloon_pages() blindly trust
the untrusted host-provided PFNs?

If a malicious host sends a DM_UNBALLOON_REQUEST specifying arbitrary PFNs
(e.g., kernel text or page tables), the guest iterates over these PFNs and
unconditionally frees them without validating if the pages were actually
ballooned out:

drivers/hv/hv_balloon.c:free_balloon_pages() {
	...
	for (i = 0; i < num_pages; i++) {
		pg = pfn_to_page(i + start_frame);
		__ClearPageOffline(pg);
		__free_page(pg);
	...
}

Can this lead to an immediate guest crash or arbitrary memory corruption via
freeing active kernel or user pages to the buddy allocator?

[ ... ]

> @@ -1527,7 +1548,8 @@ static void balloon_onchannelcallback(void *context)
>  
>  			dm->state = DM_BALLOON_DOWN;
>  			balloon_down(dm,
> -				     (struct dm_unballoon_request *)recv_buffer);
> +				     (struct dm_unballoon_request *)recv_buffer,
> +				     recvlen);
>  			break;
>  
>  		case DM_MEM_HOT_ADD_REQUEST:

[Severity: High]
This is a pre-existing issue, but are worker arguments safely updated during
VMBus softirq callbacks?

The VMBus interrupt handler balloon_onchannelcallback() directly overwrites
shared worker arguments without any locking or synchronization:

drivers/hv/hv_balloon.c:balloon_onchannelcallback() {
	...
		case DM_BALLOON_REQUEST:
			...
			dm_device.balloon_wrk.num_pages = bal_msg->num_pages;
			schedule_work(&dm_device.balloon_wrk.wrk);
			break;
	...
		case DM_MEM_HOT_ADD_REQUEST:
			...
				dm->ha_wrk.ha_page_range = *ha_pg_range;
				dm->ha_wrk.ha_region_range.page_range = 0;
	...
}

If an untrusted host floods the guest with back-to-back request messages,
can worker threads operate on torn, mixed, or corrupted parameters, leading
to incorrect guest memory operations?


[Severity: High]
This isn't a bug introduced by this patch, but does balloon_remove() have a
teardown race that violates subsystem rules?

The function calls cancel_work_sync() before vmbus_close():

drivers/hv/hv_balloon.c:balloon_remove() {
	...
	cancel_work_sync(&dm->balloon_wrk.wrk);
	cancel_work_sync(&dm->ha_wrk.wrk);
	...
	if (dm_device.state != DM_INIT_ERROR) {
		disable_page_reporting();
		vmbus_close(dev->channel);
	...
}

Because the VMBus channel remains open and active, an incoming interrupt can
trigger balloon_onchannelcallback() and independently reschedule the work
item after it was already cancelled.

Could this lead to a use-after-free of a work struct and device state,
causing a kernel crash upon module unload or device unbind?


[Severity: High]
This is a pre-existing issue, but does the probe_error path leak pending
workqueues?

In balloon_probe(), balloon_connect_vsp() opens the channel, allowing the
host to immediately send messages that schedule balloon_wrk or ha_wrk. If
the subsequent kthread_run() step fails, the code jumps to probe_error,
closes the channel, and returns an error without cancelling the work items:

drivers/hv/hv_balloon.c:balloon_probe() {
	...
probe_error:
	dm_device.state = DM_INIT_ERROR;
	dm_device.thread  = NULL;
	disable_page_reporting();
	vmbus_close(dev->channel);
connect_error:
	...
}

Can this result in asynchronous execution of a leaked work item on
uninitialized or freed device state, causing a use-after-free?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260710022914.3740453-1-michael.bommarito@gmail.com?part=1

^ permalink raw reply

* [RFC PATCH] x86/apic: Fix lost IRQ during forced vector migration on Hyper-V
From: Naman Jain @ 2026-07-10  5:40 UTC (permalink / raw)
  To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
	H . Peter Anvin
  Cc: Neeraj Upadhyay, linux-kernel, linux-hyperv, Michael Kelley

On Hyper-V the MSI retarget hypercall is asynchronous. When a CPU is
taken offline, chip_data_update() frees the outgoing CPU's vector inline
because the deferred cleanup-vector mechanism is unavailable for an
offline CPU. A device interrupt raised inside the stop_machine window can
be posted to the outgoing CPU's old vIRR after that inline free, causing
two problems:

1) The completion it carries is lost, as nothing drains the old vector
   once it is freed. If it was the last in-flight completion on an
   otherwise idle queue, the command times out and the controller is
   reset. This is the functional bug.

2) When the outgoing CPU re-enables interrupts during teardown, the late
   delivery finds VECTOR_UNUSED and reevaluate_vector() logs "No irq
   handler" - harmless but noisy.

The native MSI path (msi_set_affinity()) handles this race with
VECTOR_RETRIGGERED + irq_retrigger, but the Hyper-V MSI chip carries
IRQCHIP_MOVE_DEFERRED and reaches the forced-migration else-branch in
chip_data_update() instead, so it never gets that protection.

Mirror that protection in the forced-migration path:

- Issue __apic_send_IPI(newcpu, newvec) after installing the new mapping
  so a raced completion is drained on the new target. The retarget is
  asynchronous, so the outgoing IRR is not authoritative and the IPI is
  unconditional; a spurious or duplicate ISR is harmless to the MSI
  completion-draining handlers (NVMe, netdev), which find an empty queue.
- Mark the freed slot VECTOR_RETRIGGERED so the late stray is absorbed by
  reevaluate_vector() rather than logged. The write is unconditional
  because apic_free_vector() leaves the slot pointing at this irq's stale
  desc, not an unused entry (unlike msi_set_affinity()).

The migration runs under stop_machine with interrupts disabled on all
CPUs, so any raced CQE is visible before the new CPU handles the
retrigger.

The guard is restricted to edge MSI vectors on Hyper-V: it requires
X86_HYPER_MS_HYPERV, an external vector, a non-level trigger, and an
attached msi_desc. Only the MSI retarget hypercall is asynchronous, so
this is the sole path with the race; edge IOAPIC lines retarget
synchronously via the RTE and IR-remapped interrupts via the IRTE, and
neither is touched. Narrowing to MSI also keeps the unconditional
retrigger confined to completion-draining handlers, which tolerate a
phantom ISR, rather than arbitrary edge handlers. The slot marking and
completion drain must happen where the vector is freed and the new
mapping is installed, which is why the guard lives in chip_data_update()
rather than the Hyper-V MSI chip, which cannot reach vector_irq[].

Verified on a 64-CPU Azure VM: zero "No irq handler" events across 320
CPU hotplug passes with concurrent NVMe I/O, compared to ~460 per 5
minutes before the fix. No NVMe timeouts or controller resets observed.

Fixes: e84cf6aa501c5 ("x86/apic/vector: Handle vector release on CPU unplug correctly")
Cc: stable@vger.kernel.org
Assisted-by: GitHub-Copilot:claude-opus-4.8
Signed-off-by: Naman Jain <namjain@linux.microsoft.com>
---
 arch/x86/kernel/apic/vector.c | 48 +++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/arch/x86/kernel/apic/vector.c b/arch/x86/kernel/apic/vector.c
index bddc544653999..3577e9ca23ba6 100644
--- a/arch/x86/kernel/apic/vector.c
+++ b/arch/x86/kernel/apic/vector.c
@@ -20,6 +20,7 @@
 #include <asm/i8259.h>
 #include <asm/desc.h>
 #include <asm/irq_remapping.h>
+#include <asm/hypervisor.h>
 
 #include <asm/trace/irq_vectors.h>
 
@@ -152,6 +153,7 @@ static void chip_data_update(struct irq_data *irqd, unsigned int newvec, unsigne
 	struct apic_chip_data *apicd = apic_chip_data(irqd);
 	struct irq_desc *desc = irq_data_to_desc(irqd);
 	bool managed = irqd_affinity_is_managed(irqd);
+	bool hv_retrigger = false;
 
 	lockdep_assert_held(&vector_lock);
 
@@ -181,7 +183,46 @@ static void chip_data_update(struct irq_data *irqd, unsigned int newvec, unsigne
 		apicd->prev_cpu = apicd->cpu;
 		WARN_ON_ONCE(apicd->cpu == newcpu);
 	} else {
+		/*
+		 * The outgoing CPU cannot use the deferred cleanup-vector
+		 * mechanism, so its vector is freed inline below. On Hyper-V the
+		 * MSI retarget hypercall is asynchronous, so an interrupt raised
+		 * inside the stop_machine window can be posted to the outgoing
+		 * CPU's old vIRR after the free. Two complementary steps handle
+		 * that (see also the retrigger at the end of the function):
+		 *
+		 *  - Retrigger on the new target so a raced completion is drained
+		 *    there rather than lost. The retarget is asynchronous, so the
+		 *    outgoing IRR is not authoritative and the IPI is issued
+		 *    unconditionally; a spurious ISR is harmless to
+		 *    completion-draining handlers (they find an empty queue).
+		 *  - Mark the freed slot VECTOR_RETRIGGERED so a late stray is
+		 *    absorbed by reevaluate_vector() instead of logging "No irq
+		 *    handler" while the CPU still takes interrupts during
+		 *    teardown; __setup_vector_irq() resets it on re-online.
+		 *
+		 * This mirrors msi_set_affinity()'s protection, which the Hyper-V
+		 * MSI chip bypasses via IRQCHIP_MOVE_DEFERRED. The guard is
+		 * restricted to edge MSI vectors on Hyper-V (msi_desc present):
+		 * only the MSI retarget hypercall is asynchronous, so edge IOAPIC
+		 * lines (retargeted synchronously via the RTE) and level-triggered
+		 * lines never see this race and must not be force-injected.
+		 */
+		if (hypervisor_is_type(X86_HYPER_MS_HYPERV) &&
+		    apicd->vector >= FIRST_EXTERNAL_VECTOR &&
+		    !irqd_is_level_type(irqd) && irq_data_get_msi_desc(irqd))
+			hv_retrigger = true;
 		apic_free_vector(apicd->cpu, apicd->vector, managed);
+		/*
+		 * apic_free_vector() releases the matrix bit but leaves the
+		 * outgoing CPU's vector_irq[] slot pointing at the stale desc, so
+		 * the marker is written unconditionally here. (Unlike
+		 * msi_set_affinity(), which marks a genuinely unused slot and
+		 * therefore guards with IS_ERR_OR_NULL, the slot here still holds
+		 * this irq's old desc.)
+		 */
+		if (hv_retrigger)
+			per_cpu(vector_irq, apicd->cpu)[apicd->vector] = VECTOR_RETRIGGERED;
 	}
 
 setnew:
@@ -190,6 +231,13 @@ static void chip_data_update(struct irq_data *irqd, unsigned int newvec, unsigne
 	BUG_ON(!IS_ERR_OR_NULL(per_cpu(vector_irq, newcpu)[newvec]));
 	per_cpu(vector_irq, newcpu)[newvec] = desc;
 	apic_update_irq_cfg(irqd, newvec, newcpu);
+	/*
+	 * Drain any completion that raced onto the freed vIRR by retriggering
+	 * on the new target (see the else-branch above). Issued after the new
+	 * mapping is installed so the handler is present when it is serviced.
+	 */
+	if (hv_retrigger)
+		__apic_send_IPI(newcpu, newvec);
 }
 
 static void vector_assign_managed_shutdown(struct irq_data *irqd)
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v2 1/4] hyperv: Introduce new hypercall interfaces used by Hyper-V guest IOMMU
From: Yu Zhang @ 2026-07-10  6:17 UTC (permalink / raw)
  To: Michael Kelley
  Cc: linux-kernel@vger.kernel.org, linux-hyperv@vger.kernel.org,
	iommu@lists.linux.dev, linux-pci@vger.kernel.org,
	linux-arch@vger.kernel.org, wei.liu@kernel.org, kys@microsoft.com,
	haiyangz@microsoft.com, decui@microsoft.com, longli@microsoft.com,
	joro@8bytes.org, will@kernel.org, robin.murphy@arm.com,
	bhelgaas@google.com, kwilczynski@kernel.org,
	lpieralisi@kernel.org, mani@kernel.org, robh@kernel.org,
	arnd@arndb.de, jgg@ziepe.ca, jacob.pan@linux.microsoft.com,
	tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <SN6PR02MB41579A957E1C81583BBA1119D4FE2@SN6PR02MB4157.namprd02.prod.outlook.com>

On Thu, Jul 09, 2026 at 07:06:19PM +0000, Michael Kelley wrote:
> From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> > 
> > From: Wei Liu <wei.liu@kernel.org>
> > 
> > Hyper-V guest IOMMU is a para-virtualized IOMMU based on hypercalls.
> > Introduce the hypercalls used by the child partition to interact with
> > this facility.
> > 
> > These hypercalls fall into below categories:
> > - Detection and capability: HVCALL_GET_IOMMU_CAPABILITIES is used to
> >   detect the existence and capabilities of the guest IOMMU.
> > 
> > - Device management: HVCALL_GET_LOGICAL_DEVICE_PROPERTY is used to
> >   check whether an endpoint device is managed by the guest IOMMU.
> > 
> > - Domain management: A set of hypercalls is provided to handle the
> >   creation, configuration, and deletion of guest domains, as well as
> >   the attachment/detachment of endpoint devices to/from those domains.
> > 
> > - IOTLB flushing: HVCALL_FLUSH_DEVICE_DOMAIN is used to ask Hyper-V
> >   for a domain-selective IOTLB flush (which in its handler may flush
> >   the device TLB as well).
> > 
> > Signed-off-by: Wei Liu <wei.liu@kernel.org>
> > Co-developed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > Co-developed-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > ---
> >  include/hyperv/hvgdk_mini.h |   8 +++
> >  include/hyperv/hvhdk_mini.h | 124 ++++++++++++++++++++++++++++++++++++
> >  2 files changed, 132 insertions(+)
> > 
> > diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
> > index 6a4e8b9d570f..5bdbb44da112 100644
> > --- a/include/hyperv/hvgdk_mini.h
> > +++ b/include/hyperv/hvgdk_mini.h
> > @@ -486,10 +486,16 @@ union hv_vp_assist_msr_contents {	 /*
> > HV_REGISTER_VP_ASSIST_PAGE */
> >  #define HVCALL_GET_VP_INDEX_FROM_APIC_ID		0x009a
> >  #define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_SPACE	0x00af
> >  #define HVCALL_FLUSH_GUEST_PHYSICAL_ADDRESS_LIST	0x00b0
> > +#define HVCALL_CREATE_DEVICE_DOMAIN			0x00b1
> > +#define HVCALL_ATTACH_DEVICE_DOMAIN			0x00b2
> >  #define HVCALL_SIGNAL_EVENT_DIRECT			0x00c0
> >  #define HVCALL_POST_MESSAGE_DIRECT			0x00c1
> >  #define HVCALL_DISPATCH_VP				0x00c2
> > +#define HVCALL_DETACH_DEVICE_DOMAIN			0x00c4
> > +#define HVCALL_DELETE_DEVICE_DOMAIN			0x00c5
> >  #define HVCALL_GET_GPA_PAGES_ACCESS_STATES		0x00c9
> > +#define HVCALL_CONFIGURE_DEVICE_DOMAIN			0x00ce
> > +#define HVCALL_FLUSH_DEVICE_DOMAIN			0x00d0
> >  #define HVCALL_ACQUIRE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d7
> >  #define HVCALL_RELEASE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d8
> >  #define HVCALL_MODIFY_SPARSE_GPA_PAGE_HOST_VISIBILITY	0x00db
> > @@ -502,6 +508,8 @@ union hv_vp_assist_msr_contents {	 /*
> > HV_REGISTER_VP_ASSIST_PAGE */
> >  #define HVCALL_MMIO_READ				0x0106
> >  #define HVCALL_MMIO_WRITE				0x0107
> >  #define HVCALL_DISABLE_HYP_EX                           0x010f
> > +#define HVCALL_GET_IOMMU_CAPABILITIES			0x0125
> > +#define HVCALL_GET_LOGICAL_DEVICE_PROPERTY		0x0127
> >  #define HVCALL_MAP_STATS_PAGE2				0x0131
> > 
> >  /* HV_HYPERCALL_INPUT */
> > diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
> > index b4cb2fa26e9b..493608e791b4 100644
> > --- a/include/hyperv/hvhdk_mini.h
> > +++ b/include/hyperv/hvhdk_mini.h
> > @@ -547,4 +547,128 @@ union hv_device_id {		/* HV_DEVICE_ID */
> >  	} acpi;
> >  } __packed;
> > 
> > +/* Device domain types */
> > +#define HV_DEVICE_DOMAIN_TYPE_S1	1 /* Stage 1 domain */
> > +
> > +/* ID for default domain and NULL domain */
> > +#define HV_DEVICE_DOMAIN_ID_DEFAULT 0
> > +#define HV_DEVICE_DOMAIN_ID_NULL    0xFFFFFFFFULL
> > +
> > +union hv_device_domain_id {
> > +	u64 as_uint64;
> > +	struct {
> > +		u32 type: 4;
> > +		u32 reserved: 28;
> > +		u32 id;
> > +	} __packed;
> > +};
> > +
> > +struct hv_input_device_domain {
> > +	u64 partition_id;
> > +	union hv_input_vtl owner_vtl;
> > +	u8 padding[7];
> > +	union hv_device_domain_id domain_id;
> > +} __packed;
> > +
> > +union hv_create_device_domain_flags {
> > +	u32 as_uint32;
> > +	struct {
> > +		u32 forward_progress_required: 1;
> > +		u32 inherit_owning_vtl: 1;
> > +		u32 reserved: 30;
> > +	} __packed;
> > +};
> > +
> > +struct hv_input_create_device_domain {
> > +	struct hv_input_device_domain device_domain;
> > +	union hv_create_device_domain_flags create_device_domain_flags;
> > +} __packed;
> > +
> > +struct hv_input_delete_device_domain {
> > +	struct hv_input_device_domain device_domain;
> > +} __packed;
> > +
> > +struct hv_input_attach_device_domain {
> > +	struct hv_input_device_domain device_domain;
> > +	union hv_device_id device_id;
> > +} __packed;
> > +
> > +struct hv_input_detach_device_domain {
> > +	u64 partition_id;
> > +	union hv_device_id device_id;
> > +} __packed;
> > +
> > +struct hv_device_domain_settings {
> > +	struct {
> > +		/*
> > +		 * Enable translations. If not enabled, all transaction bypass
> > +		 * S1 translations.
> > +		 */
> > +		u64 translation_enabled: 1;
> > +		u64 blocked: 1;
> > +		/*
> > +		 * First stage address translation paging mode:
> > +		 * 0: 4-level paging (default)
> > +		 * 1: 5-level paging
> > +		 */
> > +		u64 first_stage_paging_mode: 1;
> > +		u64 reserved: 61;
> > +	} flags;
> > +
> > +	/* Address of translation table */
> > +	u64 page_table_root;
> > +} __packed;
> > +
> > +struct hv_input_configure_device_domain {
> > +	struct hv_input_device_domain device_domain;
> > +	struct hv_device_domain_settings settings;
> > +} __packed;
> > +
> > +struct hv_input_get_iommu_capabilities {
> > +	u64 partition_id;
> > +	u64 reserved;
> > +} __packed;
> > +
> > +struct hv_output_get_iommu_capabilities {
> > +	u32 size;
> > +	u16 reserved;
> > +	u8  max_iova_width;
> > +	u8  max_pasid_width;
> > +
> > +#define HV_IOMMU_CAP_PRESENT (1ULL << 0)
> > +#define HV_IOMMU_CAP_S2 (1ULL << 1)
> > +#define HV_IOMMU_CAP_S1 (1ULL << 2)
> > +#define HV_IOMMU_CAP_S1_5LVL (1ULL << 3)
> > +#define HV_IOMMU_CAP_PASID (1ULL << 4)
> > +#define HV_IOMMU_CAP_ATS (1ULL << 5)
> > +#define HV_IOMMU_CAP_PRI (1ULL << 6)
> 
> hvgdk_mini.h mostly uses the Linux BIT() and BIT_ULL()
> macros instead of explicit shifts. But this is hvhdk_mini.h.
> Does it need to play by a different set of rules for
> compatibility with the original Windows files? I think
> the Linux BIT* macros are preferable if possible. 
> 

I'm not aware of any compatibility requirement. And since it
does not change the real definition, I guess we should use
BIT*.

> > +
> > +	u64 iommu_cap;
> > +	u64 pgsize_bitmap;
> > +} __packed;
> > +
> > +enum hv_logical_device_property_code {
> > +	HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU = 10,
> > +};
> > +
> > +struct hv_input_get_logical_device_property {
> > +	u64 partition_id;
> > +	u64 logical_device_id;
> > +	/* Takes values from enum hv_logical_device_property_code. */
> > +	u32 code;
> > +	u32 reserved;
> > +} __packed;
> > +
> > +struct hv_output_get_logical_device_property {
> > +#define HV_DEVICE_IOMMU_ENABLED (1ULL << 0)
> 
> Same here about BIT_ULL().
> 

Will fix. Thanks!

B.R.
Yu

> > +	u64 device_iommu;
> > +	u64 reserved;
> > +} __packed;
> > +
> > +struct hv_input_flush_device_domain {
> > +	struct hv_input_device_domain device_domain;
> > +	u32 flags;
> > +	u32 reserved;
> > +} __packed;
> > +
> >  #endif /* _HV_HVHDK_MINI_H */
> > --
> > 2.52.0
> > 
> 

^ permalink raw reply

* Re: [PATCH v2 4/4] iommu/hyperv: Add page-selective IOTLB flush support
From: Yu Zhang @ 2026-07-10  6:22 UTC (permalink / raw)
  To: Michael Kelley
  Cc: linux-kernel@vger.kernel.org, linux-hyperv@vger.kernel.org,
	iommu@lists.linux.dev, linux-pci@vger.kernel.org,
	linux-arch@vger.kernel.org, wei.liu@kernel.org, kys@microsoft.com,
	haiyangz@microsoft.com, decui@microsoft.com, longli@microsoft.com,
	joro@8bytes.org, will@kernel.org, robin.murphy@arm.com,
	bhelgaas@google.com, kwilczynski@kernel.org,
	lpieralisi@kernel.org, mani@kernel.org, robh@kernel.org,
	arnd@arndb.de, jgg@ziepe.ca, jacob.pan@linux.microsoft.com,
	tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <SN6PR02MB415780D9D86B04EA1AE1B2BBD4FE2@SN6PR02MB4157.namprd02.prod.outlook.com>

On Thu, Jul 09, 2026 at 07:08:47PM +0000, Michael Kelley wrote:
> From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> > 
> > Add page-selective IOTLB flush using HVCALL_FLUSH_DEVICE_DOMAIN_LIST.
> > This hypercall accepts a list of (page_number, page_mask_shift) entries,
> > enabling finer-grained IOTLB invalidation compared to the domain-wide
> > HVCALL_FLUSH_DEVICE_DOMAIN used by hv_iommu_flush_iotlb_all().
> > 
> > hv_iommu_calc_flush_range() computes the smallest power-of-two aligned
> > range that covers the target IOVA region, producing a single flush
> > descriptor. This may over-flush when the range is not naturally aligned,
> > matching the approach used by Intel VT-d PSI. If the page-selective
> > flush fails, the code falls back to a full domain flush.
> > 
> > Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > ---
> >  drivers/iommu/hyperv/iommu.c | 68 +++++++++++++++++++++++++++++++++++-
> >  include/hyperv/hvgdk_mini.h  |  1 +
> >  include/hyperv/hvhdk_mini.h  | 17 +++++++++
> >  3 files changed, 85 insertions(+), 1 deletion(-)
> > 
> > diff --git a/drivers/iommu/hyperv/iommu.c b/drivers/iommu/hyperv/iommu.c
> > index 254136946404..e9b104a322fd 100644
> > --- a/drivers/iommu/hyperv/iommu.c
> > +++ b/drivers/iommu/hyperv/iommu.c
> > @@ -9,6 +9,7 @@
> >  #define pr_fmt(fmt) "Hyper-V pvIOMMU: " fmt
> >  #define dev_fmt(fmt) pr_fmt(fmt)
> > 
> > +#include <linux/hyperv.h>
> >  #include <linux/iommu.h>
> >  #include <linux/pci.h>
> >  #include <linux/dma-map-ops.h>
> > @@ -401,10 +402,74 @@ static void hv_iommu_flush_iotlb_all(struct iommu_domain *domain)
> >  	hv_flush_device_domain(to_hv_iommu_domain(domain));
> >  }
> > 
> > +/*
> > + * Calculate the minimal power-of-two aligned range that covers [start, end]
> > + * (end is inclusive). Returns a single (page_number, page_mask_shift)
> > + * descriptor that may over-flush when the range is not naturally aligned.
> > + */
> > +static void hv_iommu_calc_flush_range(unsigned long start, unsigned long end,
> > +				       union hv_iommu_flush_va *va)
> > +{
> > +	unsigned long start_pfn = HVPFN_DOWN(start);
> > +	unsigned long last_pfn = HVPFN_UP(end + 1) - 1;
> > +	unsigned long mask_shift, aligned_pfn;
> > +
> > +	if (start_pfn == last_pfn) {
> > +		mask_shift = 0;
> > +	} else {
> > +		/*
> > +		 * Find the highest bit position where start_pfn and last_pfn
> > +		 * differ.  A range aligned to one above that bit is the
> > +		 * smallest power-of-two region that covers both endpoints.
> > +		 */
> > +		mask_shift = __fls(start_pfn ^ last_pfn) + 1;
> > +	}
> > +
> > +	aligned_pfn = ALIGN_DOWN(start_pfn, 1UL << mask_shift);
> > +	va->page_number = aligned_pfn;
> > +	va->page_mask_shift = mask_shift;
> > +}
> > +
> > +static void hv_flush_device_domain_list(struct hv_iommu_domain *hv_domain,
> > +					struct iommu_iotlb_gather *iotlb_gather)
> > +{
> > +	u64 status;
> > +	unsigned long flags;
> > +	struct hv_input_flush_device_domain_list *input;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +
> > +	input->device_domain = hv_domain->device_domain;
> > +	input->flags |= HV_FLUSH_DEVICE_DOMAIN_LIST_IOMMU_FORMAT;
> > +	hv_iommu_calc_flush_range(iotlb_gather->start, iotlb_gather->end,
> > +				  &input->iova_list[0]);
> > +
> > +	status = hv_do_rep_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN_LIST,
> > +				     1, 0, input, NULL);
> > +
> > +	if (!hv_result_success(status)) {
> > +		/* Page-selective flush failed, fall back to full flush. */
> 
> With the selective flush now simplified to just a single entry, it really
> shouldn't fail, right? Doing a full flush as a fallback makes sense, but
> perhaps do a WARN_ON_ONCE() first so that there's an indication that
> the selective flush failed.
> 

It shouldn't fail under normal circumstances. Will add a WARN_ON_ONCE()
in case the fallback happens. Thanks!

> > +		struct hv_input_flush_device_domain *flush_all = (void *)input;
> > +
> > +		memset(flush_all, 0, sizeof(*flush_all));
> > +		flush_all->device_domain = hv_domain->device_domain;
> > +		status = hv_do_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN,
> > +					flush_all, NULL);
> > +		WARN(!hv_result_success(status),
> > +		     "HVCALL_FLUSH_DEVICE_DOMAIN fallback also failed: %lld\n",
> > +		     status);
> > +	}
> > +
> > +	local_irq_restore(flags);
> > +}
> > +
> >  static void hv_iommu_iotlb_sync(struct iommu_domain *domain,
> >  				struct iommu_iotlb_gather *iotlb_gather)
> >  {
> > -	hv_flush_device_domain(to_hv_iommu_domain(domain));
> > +	hv_flush_device_domain_list(to_hv_iommu_domain(domain), iotlb_gather);
> > 
> >  	iommu_put_pages_list(&iotlb_gather->freelist);
> >  }
> > @@ -455,6 +520,7 @@ static struct iommu_domain *hv_iommu_domain_alloc_paging(struct device *dev)
> > 
> >  	cfg.common.hw_max_vasz_lg2 = hv_iommu_device->max_iova_width;
> >  	cfg.common.hw_max_oasz_lg2 = 52;
> > +	cfg.common.features |= BIT(PT_FEAT_FLUSH_RANGE);
> >  	cfg.top_level = (hv_iommu_device->max_iova_width > 48) ? 4 : 3;
> > 
> >  	ret = pt_iommu_x86_64_init(&hv_domain->pt_iommu_x86_64, &cfg, GFP_KERNEL);
> > diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
> > index 5bdbb44da112..eaaf87171478 100644
> > --- a/include/hyperv/hvgdk_mini.h
> > +++ b/include/hyperv/hvgdk_mini.h
> > @@ -496,6 +496,7 @@ union hv_vp_assist_msr_contents {	 /*
> > HV_REGISTER_VP_ASSIST_PAGE */
> >  #define HVCALL_GET_GPA_PAGES_ACCESS_STATES		0x00c9
> >  #define HVCALL_CONFIGURE_DEVICE_DOMAIN			0x00ce
> >  #define HVCALL_FLUSH_DEVICE_DOMAIN			0x00d0
> > +#define HVCALL_FLUSH_DEVICE_DOMAIN_LIST			0x00d1
> >  #define HVCALL_ACQUIRE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d7
> >  #define HVCALL_RELEASE_SPARSE_SPA_PAGE_HOST_ACCESS	0x00d8
> >  #define HVCALL_MODIFY_SPARSE_GPA_PAGE_HOST_VISIBILITY	0x00db
> > diff --git a/include/hyperv/hvhdk_mini.h b/include/hyperv/hvhdk_mini.h
> > index 493608e791b4..f51d5d9467f1 100644
> > --- a/include/hyperv/hvhdk_mini.h
> > +++ b/include/hyperv/hvhdk_mini.h
> > @@ -671,4 +671,21 @@ struct hv_input_flush_device_domain {
> >  	u32 reserved;
> >  } __packed;
> > 
> > +union hv_iommu_flush_va {
> > +	u64 iova;
> > +	struct {
> > +		u64 page_mask_shift : 12;
> > +		u64 page_number : 52;
> > +	};
> > +} __packed;
> > +
> > +
> > +struct hv_input_flush_device_domain_list {
> > +	struct hv_input_device_domain device_domain;
> > +#define HV_FLUSH_DEVICE_DOMAIN_LIST_IOMMU_FORMAT (1 << 0)
> 
> Use BIT()?
> 

Sure. 

> > +	u32 flags;
> > +	u32 reserved;
> > +	union hv_iommu_flush_va iova_list[];
> > +} __packed;
> > +
> >  #endif /* _HV_HVHDK_MINI_H */
> > --
> > 2.52.0
> > 
> 

B.R.
Yu

^ permalink raw reply

* Re: [PATCH v2 3/4] iommu/hyperv: Add para-virtualized IOMMU support for Hyper-V guest
From: Yu Zhang @ 2026-07-10  7:34 UTC (permalink / raw)
  To: Michael Kelley
  Cc: linux-kernel@vger.kernel.org, linux-hyperv@vger.kernel.org,
	iommu@lists.linux.dev, linux-pci@vger.kernel.org,
	linux-arch@vger.kernel.org, wei.liu@kernel.org, kys@microsoft.com,
	haiyangz@microsoft.com, decui@microsoft.com, longli@microsoft.com,
	joro@8bytes.org, will@kernel.org, robin.murphy@arm.com,
	bhelgaas@google.com, kwilczynski@kernel.org,
	lpieralisi@kernel.org, mani@kernel.org, robh@kernel.org,
	arnd@arndb.de, jgg@ziepe.ca, jacob.pan@linux.microsoft.com,
	tgopinath@linux.microsoft.com,
	easwar.hariharan@linux.microsoft.com, mrathor@linux.microsoft.com
In-Reply-To: <SN6PR02MB4157253E030D477FD91B7E26D4FE2@SN6PR02MB4157.namprd02.prod.outlook.com>

On Thu, Jul 09, 2026 at 07:08:26PM +0000, Michael Kelley wrote:
> From: Yu Zhang <zhangyu1@linux.microsoft.com> Sent: Thursday, July 2, 2026 9:05 AM
> > 
> > Add a para-virtualized IOMMU driver for Linux guests running on Hyper-V.
> > This driver implements stage-1 IO translation within the guest OS.
> > It integrates with the Linux IOMMU core, utilizing Hyper-V hypercalls
> > for:
> >  - Capability discovery
> >  - Domain allocation, configuration, and deallocation
> >  - Device attachment and detachment
> >  - IOTLB invalidation
> > 
> > The driver constructs x86-compatible stage-1 IO page tables in the
> > guest memory using consolidated IO page table helpers. This allows
> > the guest to manage stage-1 translations independently of vendor-
> > specific drivers (like Intel VT-d or AMD IOMMU).
> > 
> > Hyper-V consumes this stage-1 IO page table when a device domain is
> > created and configured, and nests it with the host's stage-2 IO page
> > tables, therefore eliminating the VM exits for guest IOMMU mapping
> > operations. For unmapping operations, VM exits to perform the IOTLB
> > flush are still unavoidable.
> > 
> > To identify a device in its hypercall interface, the driver looks up the
> > logical device ID prefix registered for the device's PCI domain (see the
> > logical device ID registry in hv_common.c) and combines it with the PCI
> > function number of the endpoint device.
> > 
> > Co-developed-by: Wei Liu <wei.liu@kernel.org>
> > Signed-off-by: Wei Liu <wei.liu@kernel.org>
> > Co-developed-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > Signed-off-by: Easwar Hariharan <easwar.hariharan@linux.microsoft.com>
> > Signed-off-by: Yu Zhang <zhangyu1@linux.microsoft.com>
> > ---
> >  arch/x86/hyperv/hv_init.c       |   4 +
> >  arch/x86/include/asm/mshyperv.h |   4 +
> >  drivers/iommu/Kconfig           |   1 +
> >  drivers/iommu/hyperv/Kconfig    |  16 +
> >  drivers/iommu/hyperv/Makefile   |   1 +
> >  drivers/iommu/hyperv/iommu.c    | 620 ++++++++++++++++++++++++++++++++
> >  drivers/iommu/hyperv/iommu.h    |  51 +++
> >  7 files changed, 697 insertions(+)
> >  create mode 100644 drivers/iommu/hyperv/Kconfig
> >  create mode 100644 drivers/iommu/hyperv/iommu.c
> >  create mode 100644 drivers/iommu/hyperv/iommu.h
> > 
> > diff --git a/arch/x86/hyperv/hv_init.c b/arch/x86/hyperv/hv_init.c
> > index 55a8b6de2865..094f9f7ddb72 100644
> > --- a/arch/x86/hyperv/hv_init.c
> > +++ b/arch/x86/hyperv/hv_init.c
> > @@ -578,6 +578,10 @@ void __init hyperv_init(void)
> >  	old_setup_percpu_clockev = x86_init.timers.setup_percpu_clockev;
> >  	x86_init.timers.setup_percpu_clockev = hv_stimer_setup_percpu_clockev;
> > 
> > +#ifdef CONFIG_HYPERV_PVIOMMU
> > +	x86_init.iommu.iommu_init = hv_iommu_init;
> > +#endif
> > +
> 
> This approach to .iommu_init is a bit different from the Intel VT-d and
> AMD IOMMU initialization. Those cases detect the existence of the
> IOMMU first via a "detect" function that is called in pci_iommu_alloc().
> If the detect function finds an IOMMU, it sets .iommu_init. Any
> reason not to use the same approach for the Hyper-V pvIOMMU?
> One problem with exactly the same approach is that Hyper-V
> hypercalls aren't set up at the time pci_iommu_alloc() runs.

Yes. That's why I did not follow Intel VT-d and AMD IOMMU's approach -
the hv_hypercall_pg is not ready yet.

> So you'd have to call the "detect" function here in hyperv_init(),
> and have the detect function set .iommu_init if pvIOMMU
> support is present.
> 

The detecion of the presense and capabilities of the pvIOMMU are done
in one hypercall. But I guess we can:
- do the HVCALL_GET_IOMMU_CAPABILITIES in hyperv_init();
- check the presense and only set .iommu_init to hyperv_iommu_init()
  if pvIOMMU is present;
- and then do other capalibities check in hv_iommu_init();
- only give the error log if an pvIOMMU is present yet its capabilities
  are not legal.
So below errors will not be printed for guest kernels built with
CONFIG_HYPERV_PVIOMMU and running on a host w/o one.

> While the code currently in this patch works, it generates boot
> time errors if the kernel is built with CONFIG_HYPERV_PVIOMMU
> but run in a guest on a host without pvIOMMU support:
> 
> [    0.101673] Hyper-V pvIOMMU: HVCALL_GET_IOMMU_CAPABILITIES failed, status 2
> [    0.101675] Hyper-V pvIOMMU: HVCALL_GET_IOMMU_CAPABILITIES failed: -22
> 
> We really don't want errors if it's just the case that there's no
> pvIOMMU support. A less alarming message (at INFO level instead
> of ERROR level) about running without an IOMMU might be OK, but
> perhaps is unnecessary since you have an INFO message if the
> pvIOMMU is found and successfully initialized.
> 
> >  	hv_apic_init();
> > 
> >  	x86_init.pci.arch_init = hv_pci_init;
> > diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h
> > index f64393e853ee..20d947c2c758 100644
> > --- a/arch/x86/include/asm/mshyperv.h
> > +++ b/arch/x86/include/asm/mshyperv.h
> > @@ -313,6 +313,10 @@ static inline void mshv_vtl_return_hypercall(void) {}
> >  static inline void __mshv_vtl_return_call(struct mshv_vtl_cpu_context *vtl0) {}
> >  #endif
> > 
> > +#ifdef CONFIG_HYPERV_PVIOMMU
> > +int __init hv_iommu_init(void);
> > +#endif
> > +
> >  #include <asm-generic/mshyperv.h>
> > 
> >  #endif
> > diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig
> > index 6e07bd69467a..0d128f377929 100644
> > --- a/drivers/iommu/Kconfig
> > +++ b/drivers/iommu/Kconfig
> > @@ -195,6 +195,7 @@ config MSM_IOMMU
> >  source "drivers/iommu/amd/Kconfig"
> >  source "drivers/iommu/arm/Kconfig"
> >  source "drivers/iommu/intel/Kconfig"
> > +source "drivers/iommu/hyperv/Kconfig"
> >  source "drivers/iommu/iommufd/Kconfig"
> >  source "drivers/iommu/riscv/Kconfig"
> > 
> > diff --git a/drivers/iommu/hyperv/Kconfig b/drivers/iommu/hyperv/Kconfig
> > new file mode 100644
> > index 000000000000..8b6abbaaf9b8
> > --- /dev/null
> > +++ b/drivers/iommu/hyperv/Kconfig
> > @@ -0,0 +1,16 @@
> > +# SPDX-License-Identifier: GPL-2.0-only
> > +# HyperV paravirtualized IOMMU support
> > +config HYPERV_PVIOMMU
> > +	bool "Microsoft Hypervisor para-virtualized IOMMU support"
> > +	depends on X86_64 && HYPERV
> > +	select IOMMU_API
> > +	select GENERIC_PT
> > +	select IOMMU_PT
> > +	select IOMMU_PT_X86_64
> > +	select IOMMU_IOVA
> > +	default HYPERV
> > +	help
> > +	  Para-virtualized IOMMU driver for Linux guests running on
> > +	  Microsoft Hyper-V. Provides DMA remapping and IOTLB
> > +	  flush support to enable DMA isolation for devices
> 
> I think this is specifically "PCI devices", right?  VMBus devices
> that do DMA (storvsc and netvsc) don't use the pvIOMMU.
> The "assigned to the guest" phrase pretty much implies "PCI",
> but it would be clearer to be explicit.
> 

Right. It is specifically "PCI devices".

B.R.
Yu

> > +	  assigned to the guest.
> > diff --git a/drivers/iommu/hyperv/Makefile b/drivers/iommu/hyperv/Makefile
> > index 6ef0ef97f3dd..fefb409d976b 100644
> > --- a/drivers/iommu/hyperv/Makefile
> > +++ b/drivers/iommu/hyperv/Makefile
> > @@ -1,2 +1,3 @@
> >  # SPDX-License-Identifier: GPL-2.0
> >  obj-$(CONFIG_IRQ_REMAP) += hv-irq-remap-x86.o
> > +obj-$(CONFIG_HYPERV_PVIOMMU) += iommu.o
> > diff --git a/drivers/iommu/hyperv/iommu.c b/drivers/iommu/hyperv/iommu.c
> > new file mode 100644
> > index 000000000000..254136946404
> > --- /dev/null
> > +++ b/drivers/iommu/hyperv/iommu.c
> > @@ -0,0 +1,620 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +
> > +/*
> > + * Hyper-V IOMMU driver.
> > + *
> > + * Copyright (C) 2019, 2024-2026 Microsoft, Inc.
> > + */
> > +
> > +#define pr_fmt(fmt) "Hyper-V pvIOMMU: " fmt
> > +#define dev_fmt(fmt) pr_fmt(fmt)
> > +
> > +#include <linux/iommu.h>
> > +#include <linux/pci.h>
> > +#include <linux/dma-map-ops.h>
> > +#include <linux/generic_pt/iommu.h>
> > +#include <linux/pci-ats.h>
> > +
> > +#include <asm/iommu.h>
> > +#include <asm/hypervisor.h>
> > +#include <asm/mshyperv.h>
> > +
> > +#include "iommu.h"
> > +#include "../iommu-pages.h"
> > +
> > +struct hv_iommu_dev *hv_iommu_device;
> > +
> > +/*
> > + * Identity and blocking domains are static singletons: identity is a 1:1
> > + * passthrough with no page table, blocking rejects all DMA. Neither holds
> > + * per-IOMMU state, so one instance suffices even with multiple vIOMMUs.
> > + */
> > +static const struct iommu_domain_ops hv_iommu_identity_domain_ops;
> > +static const struct iommu_domain_ops hv_iommu_blocking_domain_ops;
> > +static struct iommu_ops hv_iommu_ops;
> > +
> > +static struct hv_iommu_domain hv_identity_domain = {
> > +	.domain = {
> > +		.type	= IOMMU_DOMAIN_IDENTITY,
> > +		.ops	= &hv_iommu_identity_domain_ops,
> > +		.owner	= &hv_iommu_ops,
> > +	},
> > +};
> > +static struct hv_iommu_domain hv_blocking_domain = {
> > +	.domain = {
> > +		.type	= IOMMU_DOMAIN_BLOCKED,
> > +		.ops	= &hv_iommu_blocking_domain_ops,
> > +		.owner	= &hv_iommu_ops,
> > +	},
> > +};
> > +
> > +static inline bool hv_iommu_present(u64 cap)
> > +{
> > +	return cap & HV_IOMMU_CAP_PRESENT;
> > +}
> > +
> > +static inline bool hv_iommu_s1_domain_supported(u64 cap)
> > +{
> > +	return cap & HV_IOMMU_CAP_S1;
> > +}
> > +
> > +static inline bool hv_iommu_5lvl_supported(u64 cap)
> > +{
> > +	return cap & HV_IOMMU_CAP_S1_5LVL;
> > +}
> > +
> > +static inline bool hv_iommu_ats_supported(u64 cap)
> > +{
> > +	return cap & HV_IOMMU_CAP_ATS;
> > +}
> > +
> > +static int hv_create_device_domain(struct hv_iommu_domain *hv_domain, u32 domain_stage)
> > +{
> > +	int ret;
> > +	u64 status;
> > +	unsigned long flags;
> > +	struct hv_input_create_device_domain *input;
> > +
> > +	ret = ida_alloc_range(&hv_iommu_device->domain_ids,
> > +			hv_iommu_device->first_domain, hv_iommu_device->last_domain,
> > +			GFP_KERNEL);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	hv_domain->device_domain.partition_id = HV_PARTITION_ID_SELF;
> > +	hv_domain->device_domain.domain_id.type = domain_stage;
> > +	hv_domain->device_domain.domain_id.id = ret;
> > +	hv_domain->hv_iommu = hv_iommu_device;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +	input->device_domain = hv_domain->device_domain;
> > +	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)) {
> > +		pr_err("HVCALL_CREATE_DEVICE_DOMAIN failed, status %lld\n", status);
> > +		ida_free(&hv_iommu_device->domain_ids, hv_domain->device_domain.domain_id.id);
> > +	}
> > +
> > +	return hv_result_to_errno(status);
> > +}
> > +
> > +static void hv_delete_device_domain(struct hv_iommu_domain *hv_domain)
> > +{
> > +	u64 status;
> > +	unsigned long flags;
> > +	struct hv_input_delete_device_domain *input;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +	input->device_domain = hv_domain->device_domain;
> > +	status = hv_do_hypercall(HVCALL_DELETE_DEVICE_DOMAIN, input, NULL);
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_DELETE_DEVICE_DOMAIN failed, status %lld\n", status);
> > +
> > +	ida_free(&hv_domain->hv_iommu->domain_ids, hv_domain->device_domain.domain_id.id);
> > +}
> > +
> > +static bool hv_iommu_capable(struct device *dev, enum iommu_cap cap)
> > +{
> > +	switch (cap) {
> > +	case IOMMU_CAP_CACHE_COHERENCY:
> > +		return true;
> > +	case IOMMU_CAP_DEFERRED_FLUSH:
> > +		return true;
> > +	default:
> > +		return false;
> > +	}
> > +}
> > +
> > +static void hv_flush_device_domain(struct hv_iommu_domain *hv_domain)
> > +{
> > +	u64 status;
> > +	unsigned long flags;
> > +	struct hv_input_flush_device_domain *input;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +	input->device_domain = hv_domain->device_domain;
> > +	status = hv_do_hypercall(HVCALL_FLUSH_DEVICE_DOMAIN, input, NULL);
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_FLUSH_DEVICE_DOMAIN failed, status %lld\n", status);
> > +}
> > +
> > +static int hv_iommu_attach_dev(struct iommu_domain *domain, struct device *dev,
> > +			       struct iommu_domain *old)
> > +{
> > +	u64 status;
> > +	u32 prefix;
> > +	unsigned long flags;
> > +	struct pci_dev *pdev;
> > +	struct hv_input_attach_device_domain *input;
> > +	struct hv_iommu_endpoint *vdev = dev_iommu_priv_get(dev);
> > +	struct hv_iommu_domain *hv_domain = to_hv_iommu_domain(domain);
> > +	int ret;
> > +
> > +	if (vdev->hv_domain == hv_domain)
> > +		return 0;
> > +
> > +	pdev = to_pci_dev(dev);
> > +	dev_dbg(dev, "attaching to domain %d\n",
> > +		hv_domain->device_domain.domain_id.id);
> > +
> > +	ret = hv_iommu_lookup_logical_dev_id(pci_domain_nr(pdev->bus), &prefix);
> > +	if (ret) {
> > +		dev_err(&pdev->dev, "no IOMMU registration for vPCI bus\n");
> > +		return ret;
> > +	}
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +	input->device_domain = hv_domain->device_domain;
> > +	input->device_id.as_uint64 = (u64)prefix | PCI_FUNC(pdev->devfn);
> > +	status = hv_do_hypercall(HVCALL_ATTACH_DEVICE_DOMAIN, input, NULL);
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_ATTACH_DEVICE_DOMAIN failed, status %lld\n", status);
> > +	else
> > +		vdev->hv_domain = hv_domain;
> > +
> > +	return hv_result_to_errno(status);
> > +}
> > +
> > +static int hv_iommu_blocking_attach_dev(struct iommu_domain *domain,
> > +					struct device *dev,
> > +					struct iommu_domain *old)
> > +{
> > +	int ret = hv_iommu_attach_dev(domain, dev, old);
> > +
> > +	/*
> > +	 * Attaching to the blocking domain only asks the hypervisor to
> > +	 * disable translation and IOPF for the device, so it cannot fail
> > +	 * unless there is a driver or hypervisor bug. Return the hypercall
> > +	 * status rather than 0 so that a failure on the DMA ownership claim
> > +	 * path (VFIO/iommufd) fails the claim instead of leaving the device
> > +	 * unblocked. WARN since such a failure indicates a bug.
> > +	 */
> > +	WARN_ON(ret);
> > +	return ret;
> > +}
> > +
> > +static int hv_iommu_get_logical_device_property(struct device *dev,
> > +					u32 code,
> > +					struct hv_output_get_logical_device_property *property)
> > +{
> > +	u64 status;
> > +	u32 prefix;
> > +	unsigned long flags;
> > +	int ret;
> > +	struct pci_dev *pdev = to_pci_dev(dev);
> > +	struct hv_input_get_logical_device_property *input;
> > +	struct hv_output_get_logical_device_property *output;
> > +
> > +	ret = hv_iommu_lookup_logical_dev_id(pci_domain_nr(pdev->bus), &prefix);
> > +	if (ret)
> > +		return ret;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	output = (struct hv_output_get_logical_device_property *)(input + 1);
> > +	memset(input, 0, sizeof(*input));
> > +	input->partition_id = HV_PARTITION_ID_SELF;
> > +	input->logical_device_id = (u64)prefix | PCI_FUNC(pdev->devfn);
> > +	input->code = code;
> > +	status = hv_do_hypercall(HVCALL_GET_LOGICAL_DEVICE_PROPERTY, input, output);
> > +	*property = *output;
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_GET_LOGICAL_DEVICE_PROPERTY failed, status %lld\n", status);
> > +
> > +	return hv_result_to_errno(status);
> > +}
> > +
> > +static struct iommu_device *hv_iommu_probe_device(struct device *dev)
> > +{
> > +	struct pci_dev *pdev;
> > +	struct hv_iommu_endpoint *vdev;
> > +	struct hv_output_get_logical_device_property device_iommu_property = {0};
> > +
> > +	if (!dev_is_pci(dev))
> > +		return ERR_PTR(-ENODEV);
> > +
> > +	pdev = to_pci_dev(dev);
> > +
> > +	if (hv_iommu_get_logical_device_property(dev,
> > +						 HV_LOGICAL_DEVICE_PROPERTY_PVIOMMU,
> > +						 &device_iommu_property) ||
> > +	    !(device_iommu_property.device_iommu & HV_DEVICE_IOMMU_ENABLED))
> > +		return ERR_PTR(-ENODEV);
> > +
> > +	vdev = kzalloc_obj(*vdev, GFP_KERNEL);
> > +	if (!vdev)
> > +		return ERR_PTR(-ENOMEM);
> > +
> > +	vdev->dev = dev;
> > +	vdev->hv_iommu = hv_iommu_device;
> > +	dev_iommu_priv_set(dev, vdev);
> > +
> > +	if (hv_iommu_ats_supported(hv_iommu_device->cap) &&
> > +	    pci_ats_supported(pdev))
> > +		pci_enable_ats(pdev, __ffs(hv_iommu_device->pgsize_bitmap));
> > +
> > +	return &vdev->hv_iommu->iommu;
> > +}
> > +
> > +static void hv_iommu_release_device(struct device *dev)
> > +{
> > +	struct hv_iommu_endpoint *vdev = dev_iommu_priv_get(dev);
> > +	struct pci_dev *pdev = to_pci_dev(dev);
> > +
> > +	if (pdev->ats_enabled)
> > +		pci_disable_ats(pdev);
> > +
> > +	dev_iommu_priv_set(dev, NULL);
> > +
> > +	kfree(vdev);
> > +}
> > +
> > +static struct iommu_group *hv_iommu_device_group(struct device *dev)
> > +{
> > +	if (dev_is_pci(dev))
> > +		return pci_device_group(dev);
> > +
> > +	WARN_ON_ONCE(1);
> > +	return generic_device_group(dev);
> > +}
> > +
> > +static int hv_configure_device_domain(struct hv_iommu_domain *hv_domain, u32 domain_type)
> > +{
> > +	u64 status;
> > +	unsigned long flags;
> > +	struct pt_iommu_x86_64_hw_info pt_info;
> > +	struct hv_input_configure_device_domain *input;
> > +
> > +	local_irq_save(flags);
> > +
> > +	input = *this_cpu_ptr(hyperv_pcpu_input_arg);
> > +	memset(input, 0, sizeof(*input));
> > +	input->device_domain = hv_domain->device_domain;
> > +	input->settings.flags.blocked = (domain_type == IOMMU_DOMAIN_BLOCKED);
> > +	/*
> > +	 * Clearing translation_enabled bypasses translation (DMA uses the GPA
> > +	 * directly), which only suits identity. The hypervisor requires paging
> > +	 * and blocked domains to keep it set.
> > +	 */
> > +	input->settings.flags.translation_enabled = (domain_type != IOMMU_DOMAIN_IDENTITY);
> > +
> > +	if (domain_type & __IOMMU_DOMAIN_PAGING) {
> > +		pt_iommu_x86_64_hw_info(&hv_domain->pt_iommu_x86_64, &pt_info);
> > +		input->settings.page_table_root = pt_info.gcr3_pt;
> > +		input->settings.flags.first_stage_paging_mode =
> > +			pt_info.levels == 5;
> > +	}
> > +	status = hv_do_hypercall(HVCALL_CONFIGURE_DEVICE_DOMAIN, input, NULL);
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_CONFIGURE_DEVICE_DOMAIN failed, status %lld\n", status);
> > +
> > +	return hv_result_to_errno(status);
> > +}
> > +
> > +static int __init hv_initialize_static_domains(void)
> > +{
> > +	int ret;
> > +	struct hv_iommu_domain *hv_domain;
> > +
> > +	/* Default stage-1 identity domain */
> > +	hv_domain = &hv_identity_domain;
> > +
> > +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ret = hv_configure_device_domain(hv_domain, IOMMU_DOMAIN_IDENTITY);
> > +	if (ret)
> > +		goto delete_identity_domain;
> > +
> > +	/* Default stage-1 blocked domain */
> > +	hv_domain = &hv_blocking_domain;
> > +
> > +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> > +	if (ret)
> > +		goto delete_identity_domain;
> > +
> > +	ret = hv_configure_device_domain(hv_domain, IOMMU_DOMAIN_BLOCKED);
> > +	if (ret)
> > +		goto delete_blocked_domain;
> > +
> > +	return 0;
> > +
> > +delete_blocked_domain:
> > +	hv_delete_device_domain(&hv_blocking_domain);
> > +delete_identity_domain:
> > +	hv_delete_device_domain(&hv_identity_domain);
> > +	return ret;
> > +}
> > +
> > +/* x86 architectural MSI address range */
> > +#define INTERRUPT_RANGE_START	(0xfee00000)
> > +#define INTERRUPT_RANGE_END	(0xfeefffff)
> 
> These same constants are also defined in the Intel and AMD
> IOMMU drivers. Bonus points for creating a common definition
> in a .h file that can be shared by all the drivers. :-)
>  
> > +static void hv_iommu_get_resv_regions(struct device *dev,
> > +		struct list_head *head)
> > +{
> > +	struct iommu_resv_region *region;
> > +
> > +	region = iommu_alloc_resv_region(INTERRUPT_RANGE_START,
> > +				      INTERRUPT_RANGE_END - INTERRUPT_RANGE_START + 1,
> > +				      0, IOMMU_RESV_MSI, GFP_KERNEL);
> > +	if (!region)
> > +		return;
> > +
> > +	list_add_tail(&region->list, head);
> > +}
> > +
> > +static void hv_iommu_flush_iotlb_all(struct iommu_domain *domain)
> > +{
> > +	hv_flush_device_domain(to_hv_iommu_domain(domain));
> > +}
> > +
> > +static void hv_iommu_iotlb_sync(struct iommu_domain *domain,
> > +				struct iommu_iotlb_gather *iotlb_gather)
> > +{
> > +	hv_flush_device_domain(to_hv_iommu_domain(domain));
> > +
> > +	iommu_put_pages_list(&iotlb_gather->freelist);
> > +}
> > +
> > +static void hv_iommu_paging_domain_free(struct iommu_domain *domain)
> > +{
> > +	struct hv_iommu_domain *hv_domain = to_hv_iommu_domain(domain);
> > +
> > +	/* Free all remaining mappings */
> > +	pt_iommu_deinit(&hv_domain->pt_iommu);
> > +
> > +	hv_delete_device_domain(hv_domain);
> > +
> > +	kfree(hv_domain);
> > +}
> > +
> > +static const struct iommu_domain_ops hv_iommu_identity_domain_ops = {
> > +	.attach_dev	= hv_iommu_attach_dev,
> > +};
> > +
> > +static const struct iommu_domain_ops hv_iommu_blocking_domain_ops = {
> > +	.attach_dev	= hv_iommu_blocking_attach_dev,
> > +};
> > +
> > +static const struct iommu_domain_ops hv_iommu_paging_domain_ops = {
> > +	.attach_dev	= hv_iommu_attach_dev,
> > +	IOMMU_PT_DOMAIN_OPS(x86_64),
> > +	.flush_iotlb_all = hv_iommu_flush_iotlb_all,
> > +	.iotlb_sync = hv_iommu_iotlb_sync,
> > +	.free = hv_iommu_paging_domain_free,
> > +};
> > +
> > +static struct iommu_domain *hv_iommu_domain_alloc_paging(struct device *dev)
> > +{
> > +	int ret;
> > +	struct hv_iommu_domain *hv_domain;
> > +	struct pt_iommu_x86_64_cfg cfg = {};
> > +
> > +	hv_domain = kzalloc_obj(*hv_domain, GFP_KERNEL);
> > +	if (!hv_domain)
> > +		return ERR_PTR(-ENOMEM);
> > +
> > +	ret = hv_create_device_domain(hv_domain, HV_DEVICE_DOMAIN_TYPE_S1);
> > +	if (ret)
> > +		goto err_free;
> > +
> > +	hv_domain->pt_iommu.nid = dev_to_node(dev);
> > +
> > +	cfg.common.hw_max_vasz_lg2 = hv_iommu_device->max_iova_width;
> > +	cfg.common.hw_max_oasz_lg2 = 52;
> > +	cfg.top_level = (hv_iommu_device->max_iova_width > 48) ? 4 : 3;
> > +
> > +	ret = pt_iommu_x86_64_init(&hv_domain->pt_iommu_x86_64, &cfg, GFP_KERNEL);
> > +	if (ret)
> > +		goto err_delete_domain;
> > +
> > +	/* Constrain to page sizes the hypervisor supports */
> > +	hv_domain->domain.pgsize_bitmap &= hv_iommu_device->pgsize_bitmap;
> > +
> > +	hv_domain->domain.ops = &hv_iommu_paging_domain_ops;
> > +
> > +	ret = hv_configure_device_domain(hv_domain, __IOMMU_DOMAIN_PAGING);
> > +	if (ret)
> > +		goto err_pt_deinit;
> > +
> > +	return &hv_domain->domain;
> > +
> > +err_pt_deinit:
> > +	pt_iommu_deinit(&hv_domain->pt_iommu);
> > +err_delete_domain:
> > +	hv_delete_device_domain(hv_domain);
> > +err_free:
> > +	kfree(hv_domain);
> > +	return ERR_PTR(ret);
> > +}
> > +
> > +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,
> > +	.release_device		  = hv_iommu_release_device,
> > +	.device_group		  = hv_iommu_device_group,
> > +	.get_resv_regions	  = hv_iommu_get_resv_regions,
> > +	.owner			  = THIS_MODULE,
> > +	.identity_domain	  = &hv_identity_domain.domain,
> > +	.blocked_domain		  = &hv_blocking_domain.domain,
> > +	.release_domain		  = &hv_blocking_domain.domain,
> > +};
> > +
> > +static int hv_iommu_detect(struct hv_output_get_iommu_capabilities *hv_iommu_cap)
> > +{
> > +	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 = (struct hv_output_get_iommu_capabilities *)(input + 1);
> > +	memset(input, 0, sizeof(*input));
> > +	input->partition_id = HV_PARTITION_ID_SELF;
> > +	status = hv_do_hypercall(HVCALL_GET_IOMMU_CAPABILITIES, input, output);
> > +	*hv_iommu_cap = *output;
> > +
> > +	local_irq_restore(flags);
> > +
> > +	if (!hv_result_success(status))
> > +		pr_err("HVCALL_GET_IOMMU_CAPABILITIES failed, status %lld\n", status);
> > +
> > +	return hv_result_to_errno(status);
> > +}
> > +
> > +static void __init hv_init_iommu_device(struct hv_iommu_dev *hv_iommu,
> > +			struct hv_output_get_iommu_capabilities *hv_iommu_cap)
> > +{
> > +	ida_init(&hv_iommu->domain_ids);
> > +
> > +	hv_iommu->cap = hv_iommu_cap->iommu_cap;
> > +	hv_iommu->max_iova_width = hv_iommu_cap->max_iova_width;
> > +	if (!hv_iommu_5lvl_supported(hv_iommu->cap) &&
> > +	    hv_iommu->max_iova_width > 48) {
> > +		pr_info("5-level paging not supported, limiting iova width to 48.\n");
> > +		hv_iommu->max_iova_width = 48;
> > +	}
> > +
> > +	hv_iommu->geometry = (struct iommu_domain_geometry) {
> > +		.aperture_start = 0,
> > +		.aperture_end = (((u64)1) << hv_iommu->max_iova_width) - 1,
> > +		.force_aperture = true,
> > +	};
> > +
> > +	hv_iommu->first_domain = HV_DEVICE_DOMAIN_ID_DEFAULT + 1;
> > +	hv_iommu->last_domain = HV_DEVICE_DOMAIN_ID_NULL - 1;
> > +	hv_iommu->pgsize_bitmap = hv_iommu_cap->pgsize_bitmap;
> > +	hv_iommu_device = hv_iommu;
> > +}
> > +
> > +int __init hv_iommu_init(void)
> > +{
> > +	int ret = 0;
> > +	struct hv_iommu_dev *hv_iommu = NULL;
> > +	struct hv_output_get_iommu_capabilities hv_iommu_cap = {0};
> > +
> > +	if (no_iommu || iommu_detected)
> > +		return -ENODEV;
> > +
> > +	if (!hv_is_hyperv_initialized())
> > +		return -ENODEV;
> > +
> > +	ret = hv_iommu_detect(&hv_iommu_cap);
> > +	if (ret) {
> > +		pr_err("HVCALL_GET_IOMMU_CAPABILITIES failed: %d\n", ret);
> 
> hv_iommu_detect() already outputs an error message in the failure case.
> 
> > +		return -ENODEV;
> > +	}
> > +
> > +	if (!hv_iommu_present(hv_iommu_cap.iommu_cap) ||
> > +	    !hv_iommu_s1_domain_supported(hv_iommu_cap.iommu_cap)) {
> > +		pr_err("IOMMU capabilities not sufficient: cap=0x%llx\n",
> > +		       hv_iommu_cap.iommu_cap);
> > +		return -ENODEV;
> > +	}
> > +
> > +	/*
> > +	 * The page table code only maps x86 page sizes (4K/2M/1G); require the
> > +	 * hypervisor to advertise a non-empty subset of exactly those.
> > +	 */
> > +	if (!hv_iommu_cap.pgsize_bitmap ||
> > +	    (hv_iommu_cap.pgsize_bitmap & ~(u64)(SZ_4K | SZ_2M | SZ_1G))) {
> > +		pr_err("unsupported page sizes: pgsize_bitmap=0x%llx\n",
> > +		       hv_iommu_cap.pgsize_bitmap);
> > +		return -ENODEV;
> > +	}
> > +
> > +	iommu_detected = 1;
> > +	pci_request_acs();
> > +
> > +	hv_iommu = kzalloc_obj(*hv_iommu, GFP_KERNEL);
> > +	if (!hv_iommu)
> > +		return -ENOMEM;
> > +
> > +	hv_init_iommu_device(hv_iommu, &hv_iommu_cap);
> > +
> > +	ret = hv_initialize_static_domains();
> > +	if (ret) {
> > +		pr_err("static domains init failed: %d\n", ret);
> > +		goto err_free;
> > +	}
> > +
> > +	ret = iommu_device_sysfs_add(&hv_iommu->iommu, NULL, NULL, "%s", "hv-iommu");
> > +	if (ret) {
> > +		pr_err("iommu_device_sysfs_add failed: %d\n", ret);
> > +		goto err_delete_static_domains;
> > +	}
> > +
> > +	ret = iommu_device_register(&hv_iommu->iommu, &hv_iommu_ops, NULL);
> > +	if (ret) {
> > +		pr_err("iommu_device_register failed: %d\n", ret);
> > +		goto err_sysfs_remove;
> > +	}
> > +
> > +	pr_info("successfully initialized\n");
> > +	return 0;
> > +
> > +err_sysfs_remove:
> > +	iommu_device_sysfs_remove(&hv_iommu->iommu);
> > +err_delete_static_domains:
> > +	hv_delete_device_domain(&hv_blocking_domain);
> > +	hv_delete_device_domain(&hv_identity_domain);
> > +err_free:
> > +	kfree(hv_iommu);
> > +	return ret;
> > +}
> > diff --git a/drivers/iommu/hyperv/iommu.h b/drivers/iommu/hyperv/iommu.h
> > new file mode 100644
> > index 000000000000..3a9f40fa2403
> > --- /dev/null
> > +++ b/drivers/iommu/hyperv/iommu.h
> > @@ -0,0 +1,51 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +
> > +/*
> > + * Hyper-V IOMMU driver.
> > + *
> > + * Copyright (C) 2024-2025, Microsoft, Inc.
> > + *
> > + */
> > +
> > +#ifndef _HYPERV_IOMMU_H
> > +#define _HYPERV_IOMMU_H
> > +
> > +struct hv_iommu_dev {
> > +	struct iommu_device iommu;
> > +	struct ida domain_ids;
> > +
> > +	/* Device configuration */
> > +	u8  max_iova_width;
> > +	u8  max_pasid_width;
> > +	u64 cap;
> > +	u64 pgsize_bitmap;
> > +
> > +	struct iommu_domain_geometry geometry;
> > +	u64 first_domain;
> > +	u64 last_domain;
> > +};
> > +
> > +struct hv_iommu_domain {
> > +	union {
> > +		struct iommu_domain    domain;
> > +		struct pt_iommu        pt_iommu;
> > +		struct pt_iommu_x86_64 pt_iommu_x86_64;
> > +	};
> > +	struct hv_iommu_dev *hv_iommu;
> > +	struct hv_input_device_domain device_domain;
> > +	u64		pgsize_bitmap;
> > +};
> > +
> > +PT_IOMMU_CHECK_DOMAIN(struct hv_iommu_domain, pt_iommu, domain);
> > +PT_IOMMU_CHECK_DOMAIN(struct hv_iommu_domain, pt_iommu_x86_64.iommu, domain);
> > +
> > +struct hv_iommu_endpoint {
> > +	struct device *dev;
> > +	struct hv_iommu_dev *hv_iommu;
> > +	struct hv_iommu_domain *hv_domain;
> > +};
> > +
> > +#define to_hv_iommu_domain(d) \
> > +	container_of(d, struct hv_iommu_domain, domain)
> > +
> > +#endif /* _HYPERV_IOMMU_H */
> > --
> > 2.52.0
> > 
> 

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox