Netdev List
 help / color / mirror / Atom feed
* Re: [PATCH iwl-net v2 1/6] ixgbe: fix SWFW semaphore timeout for X550 family
From: Simon Horman @ 2026-04-13 10:52 UTC (permalink / raw)
  To: Aleksandr Loktionov; +Cc: intel-wired-lan, anthony.l.nguyen, netdev
In-Reply-To: <20260408131154.2661818-2-aleksandr.loktionov@intel.com>

On Wed, Apr 08, 2026 at 03:11:49PM +0200, Aleksandr Loktionov wrote:
> According to FW documentation, the most time-consuming FW operation is
> Shadow RAM (SR) dump which takes up to 3.2 seconds.  For X550 family
> devices the module-update FW command can take over 4.5 s.  The default
> semaphore loop runs 200 iterations with a 5 ms sleep each, giving a
> maximum wait of 1 s -- not "200 ms" as previously stated in error.
> This is insufficient for X550 family FW update operations and causes
> spurious EBUSY failures.
> 
> Extend the SW/FW semaphore timeout from 1 s to 5 s (1000 iterations x
> 5 ms) for all three X550 variants: ixgbe_mac_X550, ixgbe_mac_X550EM_x,
> and ixgbe_mac_x550em_a.  All three share the same FW and exhibit the
> same worst-case latency.  Use three explicit mac.type comparisons rather
> than a range check so future MAC additions are not inadvertently
> captured.
> 
> The timeout variable is set immediately before the loop so the intent
> is clear, with an inline comment stating the resulting maximum delay.
> 
> Suggested-by: Soumen Karmakar <soumen.karmakar@intel.com>
> Cc: stable@vger.kernel.org
> Suggested-by: Marta Plantykow <marta.a.plantykow@intel.com>
> Signed-off-by: Aleksandr Loktionov <aleksandr.loktionov@intel.com>
> ---
> v1 -> v2:
>  - Squash with 0015 (X550EM extension); fix commit message ("200ms" was
>    wrong, actual default is 1 s); replace >= / <= range check with three
>    explicit mac.type == comparisons per Tony Nguyen.

Reviewed-by: Simon Horman <horms@kernel.org>


^ permalink raw reply

* [PATCH net V2 0/3] net/mlx5: Fixes for Socket-Direct
From: Tariq Toukan @ 2026-04-13 10:53 UTC (permalink / raw)
  To: Eric Dumazet, Jakub Kicinski, Paolo Abeni, Andrew Lunn,
	David S. Miller
  Cc: Saeed Mahameed, Tariq Toukan, Mark Bloch, Leon Romanovsky,
	Shay Drory, Simon Horman, Kees Cook, Parav Pandit,
	Patrisious Haddad, Gal Pressman, netdev, linux-rdma, linux-kernel,
	Dragos Tatulea

Hi,

This series fixes several race conditions and bugs in the mlx5
Socket-Direct (SD) single netdev flow.

Patch 1 serializes mlx5_sd_init()/mlx5_sd_cleanup() with
mlx5_devcom_comp_lock() and tracks the SD group state on the primary
device, preventing concurrent or duplicate bring-up/tear-down.

Patch 2 fixes the debugfs "multi-pf" directory being stored on the
calling device's sd struct instead of the primary's, which caused
memory leaks and recreation errors when cleanup ran from a different PF.

Patch 3 fixes a race where a secondary PF could access the primary's
auxiliary device after it had been unbound, by holding the primary's
device lock while operating on its auxiliary device.

Regards,
Tariq

V2:
- Link to V1:
  https://lore.kernel.org/all/20260330193412.53408-1-tariqt@nvidia.com/
- Reorder the patches so that "net/mlx5: SD: Serialize init/cleanup"
  is first.
- Add MLX5_SD_STATE_DESTROYING to the patch above to solve a concurrent
  edge case.
- Expend commit message of "net/mlx5e: SD, Fix race condition in
  secondary device probe/remove"

Shay Drory (3):
  net/mlx5: SD: Serialize init/cleanup
  net/mlx5: SD, Keep multi-pf debugfs entries on primary
  net/mlx5e: SD, Fix race condition in secondary device probe/remove

 .../net/ethernet/mellanox/mlx5/core/en_main.c | 18 +++--
 .../net/ethernet/mellanox/mlx5/core/lib/sd.c  | 70 ++++++++++++++++---
 .../net/ethernet/mellanox/mlx5/core/lib/sd.h  |  2 +
 3 files changed, 77 insertions(+), 13 deletions(-)


base-commit: 2dddb34dd0d07b01fa770eca89480a4da4f13153
-- 
2.44.0


^ permalink raw reply

* [PATCH net V2 1/3] net/mlx5: SD: Serialize init/cleanup
From: Tariq Toukan @ 2026-04-13 10:53 UTC (permalink / raw)
  To: Eric Dumazet, Jakub Kicinski, Paolo Abeni, Andrew Lunn,
	David S. Miller
  Cc: Saeed Mahameed, Tariq Toukan, Mark Bloch, Leon Romanovsky,
	Shay Drory, Simon Horman, Kees Cook, Parav Pandit,
	Patrisious Haddad, Gal Pressman, netdev, linux-rdma, linux-kernel,
	Dragos Tatulea
In-Reply-To: <20260413105323.186411-1-tariqt@nvidia.com>

From: Shay Drory <shayd@nvidia.com>

mlx5_sd_init() / mlx5_sd_cleanup() may run from multiple PFs in the same
Socket-Direct group. This can cause the SD bring-up/tear-down sequence
to be executed more than once or interleaved across PFs.

Protect SD init/cleanup with mlx5_devcom_comp_lock() and track the SD
group state on the primary device. Skip init if the primary is already
UP, and skip cleanup unless the primary is UP.

Fixes: 381978d28317 ("net/mlx5e: Create single netdev per SD group")
Signed-off-by: Shay Drory <shayd@nvidia.com>
Signed-off-by: Tariq Toukan <tariqt@nvidia.com>
---
 .../net/ethernet/mellanox/mlx5/core/lib/sd.c  | 35 +++++++++++++++++--
 1 file changed, 32 insertions(+), 3 deletions(-)

diff --git a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
index 954942ad93c5..a34860ad231a 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
@@ -18,6 +18,7 @@ struct mlx5_sd {
 	u8 host_buses;
 	struct mlx5_devcom_comp_dev *devcom;
 	struct dentry *dfs;
+	u8 state;
 	bool primary;
 	union {
 		struct { /* primary */
@@ -31,6 +32,12 @@ struct mlx5_sd {
 	};
 };
 
+enum mlx5_sd_state {
+	MLX5_SD_STATE_DOWN = 0,
+	MLX5_SD_STATE_UP,
+	MLX5_SD_STATE_DESTROYING,
+};
+
 static int mlx5_sd_get_host_buses(struct mlx5_core_dev *dev)
 {
 	struct mlx5_sd *sd = mlx5_get_sd(dev);
@@ -426,6 +433,7 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 	struct mlx5_core_dev *primary, *pos, *to;
 	struct mlx5_sd *sd = mlx5_get_sd(dev);
 	u8 alias_key[ACCESS_KEY_LEN];
+	struct mlx5_sd *primary_sd;
 	int err, i;
 
 	err = sd_init(dev);
@@ -440,10 +448,15 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 	if (err)
 		goto err_sd_cleanup;
 
+	mlx5_devcom_comp_lock(sd->devcom);
 	if (!mlx5_devcom_comp_is_ready(sd->devcom))
-		return 0;
+		goto out;
 
 	primary = mlx5_sd_get_primary(dev);
+	primary_sd = mlx5_get_sd(primary);
+
+	if (primary_sd->state != MLX5_SD_STATE_DOWN)
+		goto out;
 
 	for (i = 0; i < ACCESS_KEY_LEN; i++)
 		alias_key[i] = get_random_u8();
@@ -472,6 +485,9 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 		sd->group_id, mlx5_devcom_comp_get_size(sd->devcom));
 	sd_print_group(primary);
 
+	primary_sd->state = MLX5_SD_STATE_UP;
+out:
+	mlx5_devcom_comp_unlock(sd->devcom);
 	return 0;
 
 err_unset_secondaries:
@@ -481,6 +497,7 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 	sd_cmd_unset_primary(primary);
 	debugfs_remove_recursive(sd->dfs);
 err_sd_unregister:
+	mlx5_devcom_comp_unlock(sd->devcom);
 	sd_unregister(dev);
 err_sd_cleanup:
 	sd_cleanup(dev);
@@ -491,23 +508,35 @@ void mlx5_sd_cleanup(struct mlx5_core_dev *dev)
 {
 	struct mlx5_sd *sd = mlx5_get_sd(dev);
 	struct mlx5_core_dev *primary, *pos;
+	struct mlx5_sd *primary_sd = NULL;
 	int i;
 
 	if (!sd)
 		return;
 
+	mlx5_devcom_comp_lock(sd->devcom);
 	if (!mlx5_devcom_comp_is_ready(sd->devcom))
-		goto out;
+		goto out_unlock;
 
 	primary = mlx5_sd_get_primary(dev);
+	primary_sd = mlx5_get_sd(primary);
+
+	if (primary_sd->state != MLX5_SD_STATE_UP)
+		goto out_unlock;
+
 	mlx5_sd_for_each_secondary(i, primary, pos)
 		sd_cmd_unset_secondary(pos);
 	sd_cmd_unset_primary(primary);
 	debugfs_remove_recursive(sd->dfs);
 
 	sd_info(primary, "group id %#x, uncombined\n", sd->group_id);
-out:
+	primary_sd->state = MLX5_SD_STATE_DESTROYING;
+out_unlock:
+	mlx5_devcom_comp_unlock(sd->devcom);
 	sd_unregister(dev);
+	if (primary_sd)
+		/* devcom isn't ready, reset the state */
+		primary_sd->state = MLX5_SD_STATE_DOWN;
 	sd_cleanup(dev);
 }
 
-- 
2.44.0


^ permalink raw reply related

* [PATCH net V2 3/3] net/mlx5e: SD, Fix race condition in secondary device probe/remove
From: Tariq Toukan @ 2026-04-13 10:53 UTC (permalink / raw)
  To: Eric Dumazet, Jakub Kicinski, Paolo Abeni, Andrew Lunn,
	David S. Miller
  Cc: Saeed Mahameed, Tariq Toukan, Mark Bloch, Leon Romanovsky,
	Shay Drory, Simon Horman, Kees Cook, Parav Pandit,
	Patrisious Haddad, Gal Pressman, netdev, linux-rdma, linux-kernel,
	Dragos Tatulea
In-Reply-To: <20260413105323.186411-1-tariqt@nvidia.com>

From: Shay Drory <shayd@nvidia.com>

When utilizing Socket-Direct single netdev functionality the driver
resolves the actual auxiliary device using mlx5_sd_get_adev(). However,
the current implementation returns the primary ETH auxiliary device
without holding the device lock, leading to a potential race condition
where the ETH device could be unbound or removed concurrently during
probe, suspend, resume, or remove operations.[1]

Fix this by introducing mlx5_sd_put_adev() and updating
mlx5_sd_get_adev() so that secondaries devices would acquire the device
lock of the returned auxiliary device. After the lock is acquired, a
second devcom check is needed[2].
In addition, update The callers to pair the get operation with the new
put operation, ensuring the lock is held while the auxiliary device is
being operated on and released afterwards.

The "primary" designation is determined once in sd_register(). It's set
before devcom is marked ready, and it never changes after that.
In Addition, The primary path never locks a secondary: When the primary
device invoke mlx5_sd_get_adev(), it sees dev == primary and returns.
no additional lock is taken.
Therefore lock ordering is always: secondary_lock -> primary_lock. The
reverse never happens, so ABBA deadlock is impossible.

[1]
for example:
BUG: kernel NULL pointer dereference, address: 0000000000000370
PGD 0 P4D 0
Oops: Oops: 0000 [#1] SMP
CPU: 4 UID: 0 PID: 3945 Comm: bash Not tainted 6.19.0-rc3+ #1 NONE
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.13.0-0-gf21b5a4aeb02-prebuilt.qemu.org 04/01/2014
RIP: 0010:mlx5e_dcbnl_dscp_app+0x23/0x100 [mlx5_core]
Call Trace:
 <TASK>
 mlx5e_remove+0x82/0x12a [mlx5_core]
 device_release_driver_internal+0x194/0x1f0
 bus_remove_device+0xc6/0x140
 device_del+0x159/0x3c0
 ? devl_param_driverinit_value_get+0x29/0x80
 mlx5_rescan_drivers_locked+0x92/0x160 [mlx5_core]
 mlx5_unregister_device+0x34/0x50 [mlx5_core]
 mlx5_uninit_one+0x43/0xb0 [mlx5_core]
 remove_one+0x4e/0xc0 [mlx5_core]
 pci_device_remove+0x39/0xa0
 device_release_driver_internal+0x194/0x1f0
 unbind_store+0x99/0xa0
 kernfs_fop_write_iter+0x12e/0x1e0
 vfs_write+0x215/0x3d0
 ksys_write+0x5f/0xd0
 do_syscall_64+0x55/0xe90
 entry_SYSCALL_64_after_hwframe+0x4b/0x53

[2]
    CPU0 (primary)                     CPU1 (secondary)
==========================================================================
mlx5e_remove() (device_lock held)
                                     mlx5e_remove() (2nd device_lock held)
                                      mlx5_sd_get_adev()
                                       mlx5_devcom_comp_is_ready() => true
                                       device_lock(primary)
 mlx5_sd_get_adev() ==> ret adev
 _mlx5e_remove()
 mlx5_sd_cleanup()
 // mlx5e_remove finished
 // releasing device_lock
                                       //need another check here...
                                       mlx5_devcom_comp_is_ready() => false

Fixes: 381978d28317 ("net/mlx5e: Create single netdev per SD group")
Signed-off-by: Shay Drory <shayd@nvidia.com>
Signed-off-by: Tariq Toukan <tariqt@nvidia.com>
---
 .../net/ethernet/mellanox/mlx5/core/en_main.c  | 18 ++++++++++++++----
 .../net/ethernet/mellanox/mlx5/core/lib/sd.c   | 17 +++++++++++++++++
 .../net/ethernet/mellanox/mlx5/core/lib/sd.h   |  2 ++
 3 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
index 0b8b44bbcb9e..11f80158e107 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
@@ -6657,8 +6657,11 @@ static int mlx5e_resume(struct auxiliary_device *adev)
 		return err;
 
 	actual_adev = mlx5_sd_get_adev(mdev, adev, edev->idx);
-	if (actual_adev)
-		return _mlx5e_resume(actual_adev);
+	if (actual_adev) {
+		err = _mlx5e_resume(actual_adev);
+		mlx5_sd_put_adev(actual_adev, adev);
+		return err;
+	}
 	return 0;
 }
 
@@ -6698,6 +6701,8 @@ static int mlx5e_suspend(struct auxiliary_device *adev, pm_message_t state)
 		err = _mlx5e_suspend(actual_adev, false);
 
 	mlx5_sd_cleanup(mdev);
+	if (actual_adev)
+		mlx5_sd_put_adev(actual_adev, adev);
 	return err;
 }
 
@@ -6795,8 +6800,11 @@ static int mlx5e_probe(struct auxiliary_device *adev,
 		return err;
 
 	actual_adev = mlx5_sd_get_adev(mdev, adev, edev->idx);
-	if (actual_adev)
-		return _mlx5e_probe(actual_adev);
+	if (actual_adev) {
+		err = _mlx5e_probe(actual_adev);
+		mlx5_sd_put_adev(actual_adev, adev);
+		return err;
+	}
 	return 0;
 }
 
@@ -6849,6 +6857,8 @@ static void mlx5e_remove(struct auxiliary_device *adev)
 		_mlx5e_remove(actual_adev);
 
 	mlx5_sd_cleanup(mdev);
+	if (actual_adev)
+		mlx5_sd_put_adev(actual_adev, adev);
 }
 
 static const struct auxiliary_device_id mlx5e_id_table[] = {
diff --git a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
index a5e2e0a411df..6cece851b102 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
@@ -546,6 +546,10 @@ void mlx5_sd_cleanup(struct mlx5_core_dev *dev)
 	sd_cleanup(dev);
 }
 
+/* Cannot take devcom lock as a gate for device lock. ABBA deadlock:
+ * primary:  actual_adev_lock -> SD devcom comp lock
+ * secondary: SD devcom comp lock -> actual_adev_lock
+ */
 struct auxiliary_device *mlx5_sd_get_adev(struct mlx5_core_dev *dev,
 					  struct auxiliary_device *adev,
 					  int idx)
@@ -563,5 +567,18 @@ struct auxiliary_device *mlx5_sd_get_adev(struct mlx5_core_dev *dev,
 	if (dev == primary)
 		return adev;
 
+	device_lock(&primary->priv.adev[idx]->adev.dev);
+	/* In case primary finish removing its adev */
+	if (!mlx5_devcom_comp_is_ready(sd->devcom)) {
+		device_unlock(&primary->priv.adev[idx]->adev.dev);
+		return NULL;
+	}
 	return &primary->priv.adev[idx]->adev;
 }
+
+void mlx5_sd_put_adev(struct auxiliary_device *actual_adev,
+		      struct auxiliary_device *adev)
+{
+	if (actual_adev != adev)
+		device_unlock(&actual_adev->dev);
+}
diff --git a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.h b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.h
index 137efaf9aabc..9bfd5b9756b5 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.h
+++ b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.h
@@ -15,6 +15,8 @@ struct mlx5_core_dev *mlx5_sd_ch_ix_get_dev(struct mlx5_core_dev *primary, int c
 struct auxiliary_device *mlx5_sd_get_adev(struct mlx5_core_dev *dev,
 					  struct auxiliary_device *adev,
 					  int idx);
+void mlx5_sd_put_adev(struct auxiliary_device *actual_adev,
+		      struct auxiliary_device *adev);
 
 int mlx5_sd_init(struct mlx5_core_dev *dev);
 void mlx5_sd_cleanup(struct mlx5_core_dev *dev);
-- 
2.44.0


^ permalink raw reply related

* [PATCH net V2 2/3] net/mlx5: SD, Keep multi-pf debugfs entries on primary
From: Tariq Toukan @ 2026-04-13 10:53 UTC (permalink / raw)
  To: Eric Dumazet, Jakub Kicinski, Paolo Abeni, Andrew Lunn,
	David S. Miller
  Cc: Saeed Mahameed, Tariq Toukan, Mark Bloch, Leon Romanovsky,
	Shay Drory, Simon Horman, Kees Cook, Parav Pandit,
	Patrisious Haddad, Gal Pressman, netdev, linux-rdma, linux-kernel,
	Dragos Tatulea
In-Reply-To: <20260413105323.186411-1-tariqt@nvidia.com>

From: Shay Drory <shayd@nvidia.com>

mlx5_sd_init() creates the "multi-pf" debugfs directory under the
primary device debugfs root, but stored the dentry in the calling
device's sd struct. When sd_cleanup() run on a different PF,
this leads to using the wrong sd->dfs for removing entries, which
results in memory leak and an error in when re-creating the SD.[1]

Fix it by explicitly storing the debugfs dentry in the primary
device sd struct and use it for all per-group files.

[1]
debugfs: 'multi-pf' already exists in '0000:08:00.1'

Fixes: 4375130bf527 ("net/mlx5: SD, Add debugfs")
Signed-off-by: Shay Drory <shayd@nvidia.com>
Signed-off-by: Tariq Toukan <tariqt@nvidia.com>
---
 .../net/ethernet/mellanox/mlx5/core/lib/sd.c  | 20 ++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
index a34860ad231a..a5e2e0a411df 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/lib/sd.c
@@ -465,9 +465,13 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 	if (err)
 		goto err_sd_unregister;
 
-	sd->dfs = debugfs_create_dir("multi-pf", mlx5_debugfs_get_dev_root(primary));
-	debugfs_create_x32("group_id", 0400, sd->dfs, &sd->group_id);
-	debugfs_create_file("primary", 0400, sd->dfs, primary, &dev_fops);
+	primary_sd->dfs =
+		debugfs_create_dir("multi-pf",
+				   mlx5_debugfs_get_dev_root(primary));
+	debugfs_create_x32("group_id", 0400, primary_sd->dfs,
+			   &primary_sd->group_id);
+	debugfs_create_file("primary", 0400, primary_sd->dfs, primary,
+			    &dev_fops);
 
 	mlx5_sd_for_each_secondary(i, primary, pos) {
 		char name[32];
@@ -477,7 +481,8 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 			goto err_unset_secondaries;
 
 		snprintf(name, sizeof(name), "secondary_%d", i - 1);
-		debugfs_create_file(name, 0400, sd->dfs, pos, &dev_fops);
+		debugfs_create_file(name, 0400, primary_sd->dfs, pos,
+				    &dev_fops);
 
 	}
 
@@ -495,7 +500,8 @@ int mlx5_sd_init(struct mlx5_core_dev *dev)
 	mlx5_sd_for_each_secondary_to(i, primary, to, pos)
 		sd_cmd_unset_secondary(pos);
 	sd_cmd_unset_primary(primary);
-	debugfs_remove_recursive(sd->dfs);
+	debugfs_remove_recursive(primary_sd->dfs);
+	primary_sd->dfs = NULL;
 err_sd_unregister:
 	mlx5_devcom_comp_unlock(sd->devcom);
 	sd_unregister(dev);
@@ -520,14 +526,14 @@ void mlx5_sd_cleanup(struct mlx5_core_dev *dev)
 
 	primary = mlx5_sd_get_primary(dev);
 	primary_sd = mlx5_get_sd(primary);
-
 	if (primary_sd->state != MLX5_SD_STATE_UP)
 		goto out_unlock;
 
 	mlx5_sd_for_each_secondary(i, primary, pos)
 		sd_cmd_unset_secondary(pos);
 	sd_cmd_unset_primary(primary);
-	debugfs_remove_recursive(sd->dfs);
+	debugfs_remove_recursive(primary_sd->dfs);
+	primary_sd->dfs = NULL;
 
 	sd_info(primary, "group id %#x, uncombined\n", sd->group_id);
 	primary_sd->state = MLX5_SD_STATE_DESTROYING;
-- 
2.44.0


^ permalink raw reply related

* Re: [PATCH v11 net-next 4/7] devlink: Implement devlink param multi attribute nested data values
From: Paolo Abeni @ 2026-04-13 10:54 UTC (permalink / raw)
  To: Ratheesh Kannoth, netdev, linux-kernel, linux-rdma
  Cc: sgoutham, andrew+netdev, davem, edumazet, kuba, donald.hunter,
	horms, jiri, chuck.lever, matttbe, cjubran, saeedm, leon, tariqt,
	mbloch, dtatulea
In-Reply-To: <20260409025055.1664053-5-rkannoth@marvell.com>

On 4/9/26 4:50 AM, Ratheesh Kannoth wrote:
> @@ -441,6 +448,7 @@ union devlink_param_value {
>  	u64 vu64;
>  	char vstr[__DEVLINK_PARAM_MAX_STRING_VALUE];
>  	bool vbool;
> +	struct devlink_param_u64_array u64arr;

You mentioned that you intend to handle the possible CONFIG_FRAME_WARN
with a separate patch. IMHO such patch need to be part of this series,
or things will stay broken for an undefined amount of time until such
patch is merged separatelly.

/P


^ permalink raw reply

* Re: [PATCH iwl-net v2 4/6] ixgbe: fix cls_u32 nexthdr path returning success when no entry installed
From: Simon Horman @ 2026-04-13 10:54 UTC (permalink / raw)
  To: Aleksandr Loktionov
  Cc: intel-wired-lan, anthony.l.nguyen, netdev, Marcin Szycik
In-Reply-To: <20260408131154.2661818-5-aleksandr.loktionov@intel.com>

On Wed, Apr 08, 2026 at 03:11:52PM +0200, Aleksandr Loktionov wrote:
> ixgbe_configure_clsu32() returns 0 (success) after the nexthdr loop
> even when ixgbe_clsu32_build_input() fails for every candidate entry
> and no jump-table slot is actually programmed.  Callers that test the
> return value would then falsely believe the filter was installed.
> 
> The variable 'err' already tracks the last ixgbe_clsu32_build_input()
> return value; if the loop completes with a successful break, err is 0.
> If all attempts failed, err holds the last failure code.  Change the
> unconditional 'return 0' to 'return err' so errors are propagated
> correctly.
> 
> Fixes: 1cdaaf5405ba ("ixgbe: Match on multiple headers for cls_u32 offloads")
> Signed-off-by: Aleksandr Loktionov <aleksandr.loktionov@intel.com>
> Cc: stable@vger.kernel.org
> Reviewed-by: Marcin Szycik <marcin.szycik@linux.intel.com>
> ---
> v1 -> v2:
>  - Add Fixes: tag; reroute from iwl-next to iwl-net (false-success
>    return is a user-visible correctness bug, not a cleanup).

Reviewed-by: Simon Horman <horms@kernel.org>


^ permalink raw reply

* Re: [Intel-wired-lan] [PATCH iwl-next v2 08/10] ice: program ACL entry
From: Marcin Szycik @ 2026-04-13 10:57 UTC (permalink / raw)
  To: Loktionov, Aleksandr, intel-wired-lan@lists.osuosl.org
  Cc: netdev@vger.kernel.org, Penigalapati, Sandeep, S, Ananth,
	alexander.duyck@gmail.com, Cao, Chinh T, Nguyen, Anthony L
In-Reply-To: <IA3PR11MB898666B7AB0330B3C29DC630E5582@IA3PR11MB8986.namprd11.prod.outlook.com>



On 09.04.2026 15:35, Loktionov, Aleksandr wrote:
> 
> 
>> -----Original Message-----
>> From: Intel-wired-lan <intel-wired-lan-bounces@osuosl.org> On Behalf
>> Of Marcin Szycik
>> Sent: Thursday, April 9, 2026 2:00 PM
>> To: intel-wired-lan@lists.osuosl.org
>> Cc: netdev@vger.kernel.org; Penigalapati, Sandeep
>> <sandeep.penigalapati@intel.com>; S, Ananth <ananth.s@intel.com>;
>> alexander.duyck@gmail.com; Marcin Szycik
>> <marcin.szycik@linux.intel.com>; Cao, Chinh T <chinh.t.cao@intel.com>;
>> Nguyen, Anthony L <anthony.l.nguyen@intel.com>
>> Subject: [Intel-wired-lan] [PATCH iwl-next v2 08/10] ice: program ACL
>> entry
>>
>> From: Real Valiquette <real.valiquette@intel.com>
>>
>> Complete the filter programming process; set the flow entry and action
>> into the scenario and write it to hardware. Configure the VSI for ACL
>> filters.
>>
>> Co-developed-by: Chinh Cao <chinh.t.cao@intel.com>
>> Signed-off-by: Chinh Cao <chinh.t.cao@intel.com>
>> Signed-off-by: Real Valiquette <real.valiquette@intel.com>
>> Co-developed-by: Tony Nguyen <anthony.l.nguyen@intel.com>
>> Signed-off-by: Tony Nguyen <anthony.l.nguyen@intel.com>
>> Signed-off-by: Marcin Szycik <marcin.szycik@linux.intel.com>
>> ---
>> v2:
>> * Use plain alloc instead of devm_ for ice_flow_entry::acts
>> * Use FIELD_PREP_CONST() for ICE_ACL_RX_*_MISS_CNTR
>> * Fix wrong struct ice_acl_act_entry alloc count in
>>   ice_flow_acl_add_scen_entry_sync() - was e->entry_sz, which is an
>>   unrelated value
>> * Only set acts_cnt after successful allocation in
>>   ice_flow_acl_add_scen_entry_sync()
>> * Return -EINVAL instead of -ENOSPC on wrong index in
>>   ice_acl_scen_free_entry_idx()
>> ---
>>  drivers/net/ethernet/intel/ice/ice.h          |   2 +
>>  drivers/net/ethernet/intel/ice/ice_acl.h      |  21 +
>>  .../net/ethernet/intel/ice/ice_adminq_cmd.h   |   2 +
>>  drivers/net/ethernet/intel/ice/ice_flow.h     |   3 +
>>  drivers/net/ethernet/intel/ice/ice_acl.c      |  53 ++-
>>  drivers/net/ethernet/intel/ice/ice_acl_ctrl.c | 251 +++++++++++
>>  drivers/net/ethernet/intel/ice/ice_acl_main.c |   4 +
>>  .../ethernet/intel/ice/ice_ethtool_ntuple.c   |  48 ++-
>>  drivers/net/ethernet/intel/ice/ice_flow.c     | 395
>> ++++++++++++++++++
>>  drivers/net/ethernet/intel/ice/ice_lib.c      |  10 +-
>>  10 files changed, 782 insertions(+), 7 deletions(-)
>>
>> diff --git a/drivers/net/ethernet/intel/ice/ice.h
>> b/drivers/net/ethernet/intel/ice/ice.h
>> index 9e6643931022..f9a43daf04fe 100644
>> --- a/drivers/net/ethernet/intel/ice/ice.h
>> +++ b/drivers/net/ethernet/intel/ice/ice.h
>> @@ -1061,6 +1061,8 @@ void ice_aq_prep_for_event(struct ice_pf *pf,
>> struct ice_aq_task *task,
>>  			   u16 opcode);
>>  int ice_aq_wait_for_event(struct ice_pf *pf, struct ice_aq_task
>> *task,
>>  			  unsigned long timeout);
>> +int ice_ntuple_update_list_entry(struct ice_pf *pf,
>> +				 struct ice_ntuple_fltr *input, int
>> fltr_idx);
>>  int ice_open(struct net_device *netdev);  int
>> ice_open_internal(struct net_device *netdev);  int ice_stop(struct
>> net_device *netdev); diff --git
>> a/drivers/net/ethernet/intel/ice/ice_acl.h
>> b/drivers/net/ethernet/intel/ice/ice_acl.h
>> index 3a4adcf368cf..0b5651401eb7 100644
>> --- a/drivers/net/ethernet/intel/ice/ice_acl.h
>> +++ b/drivers/net/ethernet/intel/ice/ice_acl.h
>> @@ -39,6 +39,7 @@ struct ice_acl_tbl {
>>  	DECLARE_BITMAP(avail, ICE_AQC_ACL_ALLOC_UNITS);  };
>>
>> +#define ICE_MAX_ACL_TCAM_ENTRY (ICE_AQC_ACL_TCAM_DEPTH *
>> +ICE_AQC_ACL_SLICES)
>>  enum ice_acl_entry_prio {
>>  	ICE_ACL_PRIO_LOW = 0,
>>  	ICE_ACL_PRIO_NORMAL,
>> @@ -65,6 +66,11 @@ struct ice_acl_scen {
>>  	 * participate in this scenario
>>  	 */
>>  	DECLARE_BITMAP(act_mem_bitmap, ICE_AQC_MAX_ACTION_MEMORIES);
> 
> ...
> 
>> +	/* Determine number of cascaded TCAMs */
>> +	num_cscd = DIV_ROUND_UP(scen->width,
>> ICE_AQC_ACL_KEY_WIDTH_BYTES);
>> +
>> +	entry_tcam = ICE_ACL_TBL_TCAM_IDX(scen->start);
>> +	idx = ICE_ACL_TBL_TCAM_ENTRY_IDX(scen->start + *entry_idx);
>> +
>> +	for (u8 i = 0; i < num_cscd; i++) {
>> +		/* If the key spans more than one TCAM in the case of
>> cascaded
>> +		 * TCAMs, the key and key inverts need to be properly
>> split
>> +		 * among TCAMs.E.g.bytes 0 - 4 go to an index in the
>> first TCAM
> "E.g.bytes" -> "E.g. bytes"
> 
>> +		 * and bytes 5 - 9 go to the same index in the next
>> TCAM, etc.
>> +		 * If the entry spans more than one TCAM in a cascaded
>> TCAM
>> +		 * mode, the programming of the entries in the TCAMs
>> must be in
>> +		 * reversed order - the TCAM entry of the rightmost TCAM
>> should
>> +		 * be programmed first; the TCAM entry of the leftmost
>> TCAM
>> +		 * should be programmed last.
>> +		 */
>> +		offset = num_cscd - i - 1;
>> +		memcpy(&buf.entry_key.val,
>> +		       &keys[offset * sizeof(buf.entry_key.val)],
>> +		       sizeof(buf.entry_key.val));
>> +		memcpy(&buf.entry_key_invert.val,
>> +		       &inverts[offset *
>> sizeof(buf.entry_key_invert.val)],
>> +		       sizeof(buf.entry_key_invert.val));
>> +		err = ice_aq_program_acl_entry(hw, entry_tcam + offset,
>> idx,
>> +					       &buf, NULL);
>> +		if (err) {
>> +			ice_debug(hw, ICE_DBG_ACL, "aq program acl entry
>> failed status: %d\n",
>> +				  err);
>> +			goto out;
>> +		}
>> +	}
>> +
>> +	err = ice_acl_prog_act(hw, scen, acts, acts_cnt, *entry_idx);
>> +
>> +out:
>> +	if (err) {
>> +		ice_acl_rem_entry(hw, scen, *entry_idx);
>> +		*entry_idx = 0;
>> +	}
>> +
>> +	return err;
>> +}
>> +
>> +/**
>> + * ice_acl_prog_act - Program a scenario's action memory
>> + * @hw: pointer to the HW struct
>> + * @scen: scenario to add the entry to
>> + * @acts: pointer to a buffer containing formatted actions
>> + * @acts_cnt: indicates the number of actions stored in "acts"
>> + * @entry_idx: scenario relative index of the added flow entry
>> + *
>> + * Return: 0 on success, negative on error  */ int
>> +ice_acl_prog_act(struct ice_hw *hw, struct ice_acl_scen *scen,
>> +		     struct ice_acl_act_entry *acts, u8 acts_cnt, u16
>> entry_idx) {
>> +	u8 entry_tcam, num_cscd, i, actx_idx = 0;
>> +	struct ice_aqc_actpair act_buf = {};
>> +	int err = 0;
>> +	u16 idx;
>> +
>> +	if (entry_idx >= scen->num_entry)
>> +		return -ENOSPC;
>> +
>> +	/* Determine number of cascaded TCAMs */
>> +	num_cscd = DIV_ROUND_UP(scen->width,
>> ICE_AQC_ACL_KEY_WIDTH_BYTES);
>> +
>> +	entry_tcam = ICE_ACL_TBL_TCAM_IDX(scen->start);
>> +	idx = ICE_ACL_TBL_TCAM_ENTRY_IDX(scen->start + entry_idx);
>> +
>> +	for_each_set_bit(i, scen->act_mem_bitmap,
>> ICE_AQC_MAX_ACTION_MEMORIES) {
>> +		struct ice_acl_act_mem *mem = &hw->acl_tbl->act_mems[i];
>> +
>> +		if (actx_idx >= acts_cnt)
>> +			break;
>> +		if (mem->member_of_tcam >= entry_tcam &&
>> +		    mem->member_of_tcam < entry_tcam + num_cscd) {
>> +			memcpy(&act_buf.act[0], &acts[actx_idx],
>> +			       sizeof(struct ice_acl_act_entry));
>> +
>> +			if (++actx_idx < acts_cnt) {
>> +				memcpy(&act_buf.act[1], &acts[actx_idx],
>> +				       sizeof(struct ice_acl_act_entry));
>> +			}
>> +
>> +			err = ice_aq_program_actpair(hw, i, idx,
>> &act_buf,
>> +						     NULL);
>> +			if (err) {
>> +				ice_debug(hw, ICE_DBG_ACL, "program actpair
>> failed status: %d\n",
>> +					  err);
>> +				break;
>> +			}
>> +			actx_idx++;
>> +		}
>> +	}
>> +
>> +	if (!err && actx_idx < acts_cnt)
>> +		err = -ENOSPC;
>> +
>> +	return err;
>> +}
>> +
>> +/**
>> + * ice_acl_rem_entry - Remove a flow entry from an ACL scenario
>> + * @hw: pointer to the HW struct
>> + * @scen: scenario to remove the entry from
>> + * @entry_idx: the scenario-relative index of the flow entry being
>> +removed
>> + *
>> + * Return: 0 on success, negative on error  */ int
>> +ice_acl_rem_entry(struct ice_hw *hw, struct ice_acl_scen *scen,
>> +		      u16 entry_idx)
>> +{
>> +	struct ice_aqc_actpair act_buf = {};
>> +	struct ice_aqc_acl_data buf;
>> +	u8 entry_tcam, num_cscd, i;
>> +	int err = 0;
>> +	u16 idx;
>> +
>> +	if (!scen)
>> +		return -ENOENT;
>> +
>> +	if (entry_idx >= scen->num_entry)
>> +		return -ENOSPC;
>> +
>> +	if (!test_bit(entry_idx, scen->entry_bitmap))
>> +		return -ENOENT;
>> +
>> +	/* Determine number of cascaded TCAMs */
>> +	num_cscd = DIV_ROUND_UP(scen->width,
>> ICE_AQC_ACL_KEY_WIDTH_BYTES);
>> +
>> +	entry_tcam = ICE_ACL_TBL_TCAM_IDX(scen->start);
>> +	idx = ICE_ACL_TBL_TCAM_ENTRY_IDX(scen->start + entry_idx);
>> +
>> +	/* invalidate the flow entry */
>> +	memset(&buf, 0, sizeof(buf));
>> +	for (i = 0; i < num_cscd; i++) {
>> +		err = ice_aq_program_acl_entry(hw, entry_tcam + i, idx,
>> &buf,
>> +					       NULL);
>> +		if (err)
>> +			ice_debug(hw, ICE_DBG_ACL, "AQ program ACL entry
>> failed status: %d\n",
>> +				  err);
>> +	}
>> +
>> +	for_each_set_bit(i, scen->act_mem_bitmap,
>> ICE_AQC_MAX_ACTION_MEMORIES) {
>> +		struct ice_acl_act_mem *mem = &hw->acl_tbl->act_mems[i];
>> +
>> +		if (mem->member_of_tcam >= entry_tcam &&
>> +		    mem->member_of_tcam < entry_tcam + num_cscd) {
>> +			/* Invalidate allocated action pairs */
>> +			err = ice_aq_program_actpair(hw, i, idx,
>> &act_buf,
>> +						     NULL);
>> +			if (err)
>> +				ice_debug(hw, ICE_DBG_ACL, "program actpair
>> failed status: %d\n",
>> +					  err);
>> +		}
>> +	}
>> +
>> +	ice_acl_scen_free_entry_idx(scen, entry_idx);
>> +
>> +	return err;
>> +}
>> diff --git a/drivers/net/ethernet/intel/ice/ice_acl_main.c
>> b/drivers/net/ethernet/intel/ice/ice_acl_main.c
>> index 53cca0526756..16228be574ed 100644
>> --- a/drivers/net/ethernet/intel/ice/ice_acl_main.c
>> +++ b/drivers/net/ethernet/intel/ice/ice_acl_main.c
>> @@ -280,6 +280,10 @@ int ice_acl_add_rule_ethtool(struct ice_vsi *vsi,
>> struct ethtool_rxnfc *cmd)
>>  		hw_prof->entry_h[hw_prof->cnt++][0] = entry_h;
>>  	}
>>
>> +	input->acl_fltr = true;
>> +	/* input struct is added to the HW filter list */
>> +	ice_ntuple_update_list_entry(pf, input, fsp->location);
>> +
>>  	return 0;
>>
>>  free_input:
>> diff --git a/drivers/net/ethernet/intel/ice/ice_ethtool_ntuple.c
>> b/drivers/net/ethernet/intel/ice/ice_ethtool_ntuple.c
>> index 3e79c0bf40f4..21d4f4e3a1d0 100644
>> --- a/drivers/net/ethernet/intel/ice/ice_ethtool_ntuple.c
>> +++ b/drivers/net/ethernet/intel/ice/ice_ethtool_ntuple.c
>> @@ -1791,6 +1791,21 @@ void ice_vsi_manage_fdir(struct ice_vsi *vsi,
>> bool ena)
>>  	mutex_unlock(&hw->fdir_fltr_lock);
>>  }
>>
>> +/**
>> + * ice_del_acl_ethtool - delete an ACL rule entry
>> + * @hw: pointer to HW instance
>> + * @fltr: filter structure
>> + *
>> + * Return: 0 on success, negative on error  */ static int
>> +ice_del_acl_ethtool(struct ice_hw *hw, struct ice_ntuple_fltr *fltr)
>> {
>> +	u64 entry;
>> +
>> +	entry = ice_flow_find_entry(hw, ICE_BLK_ACL, fltr->fltr_id);
>> +	return ice_flow_rem_entry(hw, ICE_BLK_ACL, entry); }
>> +
>>  /**
>>   * ice_fdir_do_rem_flow - delete flow and possibly add perfect flow
>>   * @pf: PF structure
>> @@ -1824,7 +1839,7 @@ ice_fdir_do_rem_flow(struct ice_pf *pf, enum
>> ice_fltr_ptype flow_type)
>>   *
>>   * Return: 0 on success and negative on errors
>>   */
>> -static int
>> +int
>>  ice_ntuple_update_list_entry(struct ice_pf *pf, struct
>> ice_ntuple_fltr *input,
>>  			     int fltr_idx)
>>  {
>> @@ -1843,13 +1858,36 @@ ice_ntuple_update_list_entry(struct ice_pf
>> *pf, struct ice_ntuple_fltr *input,
>>
>>  	old_fltr = ice_fdir_find_fltr_by_idx(hw, fltr_idx);
>>  	if (old_fltr) {
>> -		err = ice_fdir_write_all_fltr(pf, old_fltr, false);
>> -		if (err)
>> -			return err;
>> +		if (old_fltr->acl_fltr) {
>> +			/* ACL filter - if the input buffer is present
>> +			 * then this is an update and we don't want to
>> +			 * delete the filter from the HW. We've already
>> +			 * written the change to the HW at this point, so
>> +			 * just update the SW structures to make sure
>> +			 * everything is hunky-dory. If no input then
>> this
>> +			 * is a delete so we should delete the filter
>> from
>> +			 * the HW and clean up our SW structures.
>> +			 */
>> +			if (!input) {
>> +				err = ice_del_acl_ethtool(hw, old_fltr);
>> +				if (err)
>> +					return err;
>> +			}
>> +		} else {
>> +			/* FD filter */
>> +			err = ice_fdir_write_all_fltr(pf, old_fltr,
>> false);
>> +			if (err)
>> +				return err;
>> +		}
>> +
>>  		ice_fdir_update_cntrs(hw, old_fltr->flow_type, false,
>> false);
>>  		/* update sb-filters count, specific to ring->channel */
>>  		ice_update_per_q_fltr(vsi, old_fltr->orig_q_index,
>> false);
>> -		if (!input && !hw->fdir_fltr_cnt[old_fltr->flow_type])
>> +		/* Also delete the HW filter info if we have just
>> deleted the
>> +		 * last filter of flow_type.
>> +		 */
>> +		if (!old_fltr->acl_fltr && !input &&
>> +		    !hw->fdir_fltr_cnt[old_fltr->flow_type])
>>  			/* we just deleted the last filter of flow_type
>> so we
>>  			 * should also delete the HW filter info.
>>  			 */
>> diff --git a/drivers/net/ethernet/intel/ice/ice_flow.c
>> b/drivers/net/ethernet/intel/ice/ice_flow.c
>> index dce6d2ffcb15..144d8326d4f9 100644
>> --- a/drivers/net/ethernet/intel/ice/ice_flow.c
>> +++ b/drivers/net/ethernet/intel/ice/ice_flow.c
>> @@ -1744,6 +1744,16 @@ static int ice_flow_rem_entry_sync(struct
>> ice_hw *hw, enum ice_block blk,
>>  		return -EINVAL;
>>
>>  	if (blk == ICE_BLK_ACL) {
>> +		int err;
>> +
>> +		if (!entry->prof)
>> +			return -EINVAL;
>> +
>> +		err = ice_acl_rem_entry(hw, entry->prof->cfg.scen,
>> +					entry->scen_entry_idx);
>> +		if (err)
>> +			return err;
>> +
>>  		if (entry->acts_cnt && entry->acts)
>>  			ice_flow_acl_free_act_cntr(hw, entry->acts,
>>  						   entry->acts_cnt);
>> @@ -1879,10 +1889,34 @@ ice_flow_rem_prof_sync(struct ice_hw *hw, enum
>> ice_block blk,
>>  	}
>>
>>  	if (blk == ICE_BLK_ACL) {
>> +		struct ice_aqc_acl_prof_generic_frmt buf;
>> +		u8 prof_id = 0;
>> +
>>  		/* Disassociate the scenario from the profile for the PF
>> */
>>  		status = ice_flow_acl_disassoc_scen(hw, prof);
>>  		if (status)
>>  			return status;
>> +
>> +		status = ice_flow_get_hw_prof(hw, blk, prof->id,
>> &prof_id);
>> +		if (status)
>> +			return status;
>> +
>> +		status = ice_query_acl_prof(hw, prof_id, &buf, NULL);
>> +		if (status)
>> +			return status;
>> +
>> +		/* Clear the range-checker if the profile ID is no
>> longer
>> +		 * used by any PF
>> +		 */
>> +		if (!ice_flow_acl_is_prof_in_use(&buf)) {
>> +			/* Clear the range-checker value for profile ID
>> */
>> +			struct ice_aqc_acl_profile_ranges query_rng_buf =
>> {};
>> +
>> +			status = ice_prog_acl_prof_ranges(hw, prof_id,
>> +							  &query_rng_buf,
>> NULL);
>> +			if (status)
>> +				return status;
>> +		}
>>  	}
>>
>>  	/* Remove all hardware profiles associated with this flow
>> profile */ @@ -2214,6 +2248,44 @@ int ice_flow_rem_prof(struct ice_hw
>> *hw, enum ice_block blk, u64 prof_id)
>>  	return status;
>>  }
>>
>> +/**
>> + * ice_flow_find_entry - look for a flow entry using its unique ID
>> + * @hw: pointer to the HW struct
>> + * @blk: classification stage
>> + * @entry_id: unique ID to identify this flow entry
>> + *
>> + * Look for the flow entry with the specified unique ID in all flow
>> +profiles of
>> + * the specified classification stage.
>> + *
>> + * Return: flow entry handle if entry found, ICE_FLOW_ENTRY_ID_INVAL
>> +otherwise  */
>> +u64 ice_flow_find_entry(struct ice_hw *hw, enum ice_block blk, u64
>> +entry_id) {
>> +	struct ice_flow_entry *found = NULL;
>> +	struct ice_flow_prof *p;
>> +
>> +	mutex_lock(&hw->fl_profs_locks[blk]);
>> +
>> +	list_for_each_entry(p, &hw->fl_profs[blk], l_entry) {
>> +		struct ice_flow_entry *e;
>> +
>> +		mutex_lock(&p->entries_lock);
>> +		list_for_each_entry(e, &p->entries, l_entry)
>> +			if (e->id == entry_id) {
>> +				found = e;
>> +				break;
>> +			}
>> +		mutex_unlock(&p->entries_lock);
>> +
>> +		if (found)
>> +			break;
>> +	}
>> +
>> +	mutex_unlock(&hw->fl_profs_locks[blk]);
>> +
>> +	return found ? ICE_FLOW_ENTRY_HNDL(found) :
>> +ICE_FLOW_ENTRY_HANDLE_INVAL; }
>> +
>>  /**
>>   * ice_flow_acl_check_actions - Checks the ACL rule's actions
>>   * @hw: pointer to the hardware structure @@ -2541,6 +2613,325 @@
>> static int ice_flow_acl_frmt_entry(struct ice_hw *hw,
>>
>>  	return err;
>>  }
>> +
>> +/**
>> + * ice_flow_acl_find_scen_entry_cond - Find an ACL scenario entry
>> that matches
>> + *				       the compared data
>> + * @prof: pointer to flow profile
>> + * @e: pointer to the comparing flow entry
>> + * @do_chg_action: decide if we want to change the ACL action
>> + * @do_add_entry: decide if we want to add the new ACL entry
>> + * @do_rem_entry: decide if we want to remove the current ACL entry
>> + *
>> + * Find an ACL scenario entry that matches the compared data. Also
>> figure out:
>> + * a) If we want to change the ACL action
>> + * b) If we want to add the new ACL entry
>> + * c) If we want to remove the current ACL entry
>> + *
>> + * Return: ACL scenario entry, or NULL if not found  */ static struct
>> +ice_flow_entry * ice_flow_acl_find_scen_entry_cond(struct
>> ice_flow_prof
>> +*prof,
>> +				  struct ice_flow_entry *e, bool
>> *do_chg_action,
>> +				  bool *do_add_entry, bool *do_rem_entry) {
>> +	struct ice_flow_entry *p, *return_entry = NULL;
>> +
>> +	/* Check if:
>> +	 * a) There exists an entry with same matching data, but
>> different
>> +	 *    priority, then we remove this existing ACL entry. Then,
>> we
>> +	 *    will add the new entry to the ACL scenario.
>> +	 * b) There exists an entry with same matching data, priority,
>> and
>> +	 *    result action, then we do nothing
>> +	 * c) There exists an entry with same matching data, priority,
>> but
>> +	 *    different, action, then do only change the action's
>> entry.
> Too much of commas, please reduce the number.
> 
> 
>> +	 * d) Else, we add this new entry to the ACL scenario.
>> +	 */
>> +	*do_chg_action = false;
>> +	*do_add_entry = true;
>> +	*do_rem_entry = false;
>> +	list_for_each_entry(p, &prof->entries, l_entry) {
>> +		if (memcmp(p->entry, e->entry, p->entry_sz))
>> +			continue;
>> +
>> +		/* From this point, we have the same matching_data. */
>> +		*do_add_entry = false;
>> +		return_entry = p;
>> +
>> +		if (p->priority != e->priority) {
>> +			/* matching data && !priority */
>> +			*do_add_entry = true;
>> +			*do_rem_entry = true;
>> +			break;
>> +		}
>> +
>> +		/* From this point, we will have matching_data &&
>> priority */
>> +		if (p->acts_cnt != e->acts_cnt)
>> +			*do_chg_action = true;
>> +		for (int i = 0; i < p->acts_cnt; i++) {
>> +			bool found_not_match = false;
>> +
>> +			for (int j = 0; j < e->acts_cnt; j++)
>> +				if (memcmp(&p->acts[i], &e->acts[j],
>> +					   sizeof(struct ice_flow_action)))
> Due to comment above it should be if (!memcmp(&p->acts[i], &e->acts[j],
> Please fix the comment or code.

I believe the logic is fine, but naming/comments might be a bit confusing.
I'll try to make it more readable.

Thanks,
Marcin

> Otherwise, it looks good.
> Reviewed-by: Aleksandr Loktionov <aleksandr.loktionov@intel.com>
> 
>> {
>> +					found_not_match = true;
>> +					break;
> 
> ...
> 
>>  }
>>
>>  /**
>> --
>> 2.49.0
> 


^ permalink raw reply

* Re: [PATCH net-next v6 1/2] net: hsr: require valid EOT supervision TLV
From: Felix Maurer @ 2026-04-13 10:57 UTC (permalink / raw)
  To: luka.gejak; +Cc: davem, edumazet, kuba, pabeni, netdev, horms
In-Reply-To: <20260413103449.169913-2-luka.gejak@linux.dev>

On Mon, Apr 13, 2026 at 12:34:48PM +0200, luka.gejak@linux.dev wrote:
> From: Luka Gejak <luka.gejak@linux.dev>
>
> Supervision frames are only valid if terminated with a zero-length EOT
> TLV. The current check fails to reject non-EOT entries as the terminal
> TLV, potentially allowing malformed supervision traffic.
>
> Fix this by strictly requiring the terminal TLV to be HSR_TLV_EOT
> with a length of zero, and properly linearizing the TLV header before
> access.
>
> Assisted-by: Gemini:Gemini-3.1-flash
> Signed-off-by: Luka Gejak <luka.gejak@linux.dev>

Please respect the netdev development process [1], net-next is currently
closed. I'll leave it to the maintainers if a resubmission is required.

Other than that:
Reviewed-by: Felix Maurer <fmaurer@redhat.com>

Thanks,
   Felix


[1]: https://docs.kernel.org/process/maintainer-netdev.html#git-trees-and-patch-flow

> ---
>  net/hsr/hsr_forward.c | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
>
> diff --git a/net/hsr/hsr_forward.c b/net/hsr/hsr_forward.c
> index 0aca859c88cb..0774981a65c1 100644
> --- a/net/hsr/hsr_forward.c
> +++ b/net/hsr/hsr_forward.c
> @@ -84,7 +84,7 @@ static bool is_supervision_frame(struct hsr_priv *hsr, struct sk_buff *skb)
>
>  	/* Get next tlv */
>  	total_length += hsr_sup_tag->tlv.HSR_TLV_length;
> -	if (!pskb_may_pull(skb, total_length))
> +	if (!pskb_may_pull(skb, total_length + sizeof(struct hsr_sup_tlv)))
>  		return false;
>  	skb_pull(skb, total_length);
>  	hsr_sup_tlv = (struct hsr_sup_tlv *)skb->data;
> @@ -100,7 +100,7 @@ static bool is_supervision_frame(struct hsr_priv *hsr, struct sk_buff *skb)
>
>  		/* make sure another tlv follows */
>  		total_length += sizeof(struct hsr_sup_tlv) + hsr_sup_tlv->HSR_TLV_length;
> -		if (!pskb_may_pull(skb, total_length))
> +		if (!pskb_may_pull(skb, total_length + sizeof(struct hsr_sup_tlv)))
>  			return false;
>
>  		/* get next tlv */
> @@ -110,7 +110,7 @@ static bool is_supervision_frame(struct hsr_priv *hsr, struct sk_buff *skb)
>  	}
>
>  	/* end of tlvs must follow at the end */
> -	if (hsr_sup_tlv->HSR_TLV_type == HSR_TLV_EOT &&
> +	if (hsr_sup_tlv->HSR_TLV_type != HSR_TLV_EOT ||
>  	    hsr_sup_tlv->HSR_TLV_length != 0)
>  		return false;
>
> --
> 2.53.0
>


^ permalink raw reply

* Re: [PATCH v11 net-next 4/7] devlink: Implement devlink param multi attribute nested data values
From: Ratheesh Kannoth @ 2026-04-13 11:00 UTC (permalink / raw)
  To: Paolo Abeni
  Cc: netdev, linux-kernel, linux-rdma, sgoutham, andrew+netdev, davem,
	edumazet, kuba, donald.hunter, horms, jiri, chuck.lever, matttbe,
	cjubran, saeedm, leon, tariqt, mbloch, dtatulea
In-Reply-To: <b52ce943-18f7-4402-8b6a-3d9f69bf7d19@redhat.com>

On 2026-04-13 at 16:24:41, Paolo Abeni (pabeni@redhat.com) wrote:
> On 4/9/26 4:50 AM, Ratheesh Kannoth wrote:
> > @@ -441,6 +448,7 @@ union devlink_param_value {
> >  	u64 vu64;
> >  	char vstr[__DEVLINK_PARAM_MAX_STRING_VALUE];
> >  	bool vbool;
> > +	struct devlink_param_u64_array u64arr;
>
> You mentioned that you intend to handle the possible CONFIG_FRAME_WARN
> with a separate patch. IMHO such patch need to be part of this series,
> or things will stay broken for an undefined amount of time until such
> patch is merged separatelly.

Patch no: 3 in the same series.
https://lore.kernel.org/netdev/20260409025055.1664053-4-rkannoth@marvell.com/#t

>
> /P
>

^ permalink raw reply

* [PATCH v2] net/sched: sch_cake: fix NAT destination port not being updated in cake_update_flowkeys
From: Dudu Lu @ 2026-04-13 11:00 UTC (permalink / raw)
  To: netdev; +Cc: toke, jhs, jiri, Dudu Lu

cake_update_flowkeys() is supposed to update the flow dissector keys
with the NAT-translated addresses and ports from conntrack, so that
CAKE's per-flow fairness correctly identifies post-NAT flows as
belonging to the same connection.

For the source port, this works correctly:
    keys->ports.src = port;

But for the destination port, the assignment is reversed:
    port = keys->ports.dst;

This means the NAT destination port is never updated in the flow keys.
As a result, when multiple connections are NATed to the same destination,
CAKE treats them as separate flows because the original (pre-NAT)
destination ports differ. This breaks CAKE's NAT-aware flow isolation
when using the "nat" mode.

The bug was introduced in commit b0c19ed6088a ("sch_cake: Take advantage
of skb->hash where appropriate") which refactored the original direct
assignment into a compare-and-conditionally-update pattern, but wrote
the destination port update backwards.

Fix by reversing the assignment direction to match the source port
pattern.

Fixes: b0c19ed6088a ("sch_cake: Take advantage of skb->hash where appropriate")
Signed-off-by: Dudu Lu <phx0fer@gmail.com>
---
 net/sched/sch_cake.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
index 9efe23f8371b..4ac6c36ca6e4 100644
--- a/net/sched/sch_cake.c
+++ b/net/sched/sch_cake.c
@@ -619,7 +619,7 @@ static bool cake_update_flowkeys(struct flow_keys *keys,
 		}
 		port = rev ? tuple.src.u.all : tuple.dst.u.all;
 		if (port != keys->ports.dst) {
-			port = keys->ports.dst;
+			keys->ports.dst = port;
 			upd = true;
 		}
 	}
-- 
2.39.3 (Apple Git-145)


^ permalink raw reply related

* RE: [Intel-wired-lan] [PATCH net] idpf: fix double free and use-after-free in aux device error paths
From: Loktionov, Aleksandr @ 2026-04-13 11:06 UTC (permalink / raw)
  To: Greg Kroah-Hartman, intel-wired-lan@lists.osuosl.org
  Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
	Nguyen, Anthony L, Kitszel, Przemyslaw, Andrew Lunn,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	stable
In-Reply-To: <2026041116-retail-bagginess-250f@gregkh>



> -----Original Message-----
> From: Intel-wired-lan <intel-wired-lan-bounces@osuosl.org> On Behalf
> Of Greg Kroah-Hartman
> Sent: Saturday, April 11, 2026 12:12 PM
> To: intel-wired-lan@lists.osuosl.org
> Cc: netdev@vger.kernel.org; linux-kernel@vger.kernel.org; Greg Kroah-
> Hartman <gregkh@linuxfoundation.org>; Nguyen, Anthony L
> <anthony.l.nguyen@intel.com>; Kitszel, Przemyslaw
> <przemyslaw.kitszel@intel.com>; Andrew Lunn <andrew+netdev@lunn.ch>;
> David S. Miller <davem@davemloft.net>; Eric Dumazet
> <edumazet@google.com>; Jakub Kicinski <kuba@kernel.org>; Paolo Abeni
> <pabeni@redhat.com>; stable <stable@kernel.org>
> Subject: [Intel-wired-lan] [PATCH net] idpf: fix double free and use-
> after-free in aux device error paths
> 
> When auxiliary_device_add() fails in idpf_plug_vport_aux_dev() or
> idpf_plug_core_aux_dev(), the err_aux_dev_add label calls
> auxiliary_device_uninit() and falls through to err_aux_dev_init.  The
> uninit call will trigger put_device(), which invokes the release
> callback (idpf_vport_adev_release / idpf_core_adev_release) that frees
> iadev.  The fall-through then reads adev->id from the freed iadev for
> ida_free() and double-frees iadev with kfree().
> 
> Free the IDA slot and clear the back-pointer before uninit, while adev
> is still valid, then return immediately.
> 
> Commit 65637c3a1811 65637c3a1811 ("idpf: fix UAF in RDMA core aux dev
> deinitialization") fixed the same use-after-free in the matching
> unplug path in this file but missed both probe error paths.
> 
> Cc: Tony Nguyen <anthony.l.nguyen@intel.com>
> Cc: Przemek Kitszel <przemyslaw.kitszel@intel.com>
> Cc: Andrew Lunn <andrew+netdev@lunn.ch>
> Cc: "David S. Miller" <davem@davemloft.net>
> Cc: Eric Dumazet <edumazet@google.com>
> Cc: Jakub Kicinski <kuba@kernel.org>
> Cc: Paolo Abeni <pabeni@redhat.com>
> Cc: stable <stable@kernel.org>
> Fixes: be91128c579c ("idpf: implement RDMA vport auxiliary dev create,
> init, and destroy")
> Fixes: f4312e6bfa2a ("idpf: implement core RDMA auxiliary dev create,
> init, and destroy")
> Assisted-by: gregkh_clanker_t1000
> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
> ---
> Note, these cleanup paths are messy, but I couldn't see a simpler way
> without a lot more rework, so I choose the simple way :)
> 
>  drivers/net/ethernet/intel/idpf/idpf_idc.c | 6 ++++++
>  1 file changed, 6 insertions(+)
> 
> diff --git a/drivers/net/ethernet/intel/idpf/idpf_idc.c
> b/drivers/net/ethernet/intel/idpf/idpf_idc.c
> index 7e4f4ac92653..b7d6b08fc89e 100644
> --- a/drivers/net/ethernet/intel/idpf/idpf_idc.c
> +++ b/drivers/net/ethernet/intel/idpf/idpf_idc.c
> @@ -90,7 +90,10 @@ static int idpf_plug_vport_aux_dev(struct
> iidc_rdma_core_dev_info *cdev_info,
>  	return 0;
> 
>  err_aux_dev_add:
> +	ida_free(&idpf_idc_ida, adev->id);
> +	vdev_info->adev = NULL;
>  	auxiliary_device_uninit(adev);
> +	return ret;
>  err_aux_dev_init:
>  	ida_free(&idpf_idc_ida, adev->id);
>  err_ida_alloc:
> @@ -228,7 +231,10 @@ static int idpf_plug_core_aux_dev(struct
> iidc_rdma_core_dev_info *cdev_info)
>  	return 0;
> 
>  err_aux_dev_add:
> +	ida_free(&idpf_idc_ida, adev->id);
> +	cdev_info->adev = NULL;
>  	auxiliary_device_uninit(adev);
> +	return ret;
>  err_aux_dev_init:
>  	ida_free(&idpf_idc_ida, adev->id);
>  err_ida_alloc:
> --
> 2.53.0

Reviewed-by: Aleksandr Loktionov <aleksandr.loktionov@intel.com>

^ permalink raw reply

* [PATCH v3 net] net: ax25: fix integer overflow in ax25_rx_fragment()
From: Mashiro Chen @ 2026-04-13 11:14 UTC (permalink / raw)
  To: netdev; +Cc: linux-hams, kuba, horms, davem, pabeni, edumazet, Mashiro Chen
In-Reply-To: <20260409025026.24575-1-mashiro.chen@mailbox.org>

ax25_rx_fragment() accumulates fragment lengths into ax25_cb->fraglen,
which is an unsigned short. When the total exceeds 65535, fraglen wraps
around to a small value. The subsequent alloc_skb(fraglen) allocates a
too-small buffer, and skb_put() in the copy loop triggers skb_over_panic().

Add pskb_may_pull(skb, 1) at function entry to ensure the segmentation
header byte is in the linear data area before dereferencing skb->data.
This also rejects zero-length skbs, which the original code did not
check for.

Three issues in the overflow error path are also fixed:
First, the current skb, after skb_pull(skb, 1), is neither enqueued
nor freed before returning 1, leaking it. Add kfree_skb(skb) before
the return.
Second, ax25->fraglen is not reset after skb_queue_purge(). Add
ax25->fraglen = 0 to restore a consistent state.
Third, the explicit (unsigned int) cast on fraglen is unnecessary: the
addition with skb->len (unsigned int) promotes fraglen automatically.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Mashiro Chen <mashiro.chen@mailbox.org>
---
 net/ax25/ax25_in.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/net/ax25/ax25_in.c b/net/ax25/ax25_in.c
index 68202c19b19e3f..e1834e11bb0b6a 100644
--- a/net/ax25/ax25_in.c
+++ b/net/ax25/ax25_in.c
@@ -35,15 +35,20 @@ static int ax25_rx_fragment(ax25_cb *ax25, struct sk_buff *skb)
 {
 	struct sk_buff *skbn, *skbo;
 
+	if (!pskb_may_pull(skb, 1))
+		return 0;
+
 	if (ax25->fragno != 0) {
 		if (!(*skb->data & AX25_SEG_FIRST)) {
 			if ((ax25->fragno - 1) == (*skb->data & AX25_SEG_REM)) {
 				/* Enqueue fragment */
 				ax25->fragno = *skb->data & AX25_SEG_REM;
 				skb_pull(skb, 1);	/* skip fragno */
-				if ((unsigned int)ax25->fraglen + skb->len > USHRT_MAX) {
+				if (ax25->fraglen + skb->len > USHRT_MAX) {
+					kfree_skb(skb);
 					skb_queue_purge(&ax25->frag_queue);
 					ax25->fragno = 0;
+					ax25->fraglen = 0;
 					return 1;
 				}
 				ax25->fraglen += skb->len;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2] dpf: fix UAF and double free in idpf_plug_vport_aux_dev() error path
From: Guangshuo Li @ 2026-04-13 11:20 UTC (permalink / raw)
  To: Tony Nguyen, Przemek Kitszel, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Joshua Hay,
	Tatyana Nikolova, Madhu Chittim, intel-wired-lan, netdev,
	linux-kernel
  Cc: Guangshuo Li, stable

If auxiliary_device_add() fails, idpf_plug_vport_aux_dev() calls
auxiliary_device_uninit(adev), whose release callback
idpf_vport_adev_release() frees the containing
struct iidc_rdma_vport_auxiliary_dev.

The current error path then accesses adev->id and later frees iadev
again, which may lead to a use-after-free and double free.

The issue was identified by a static analysis tool I developed and
confirmed by manual review.

Fix it by storing the allocated auxiliary device id in a local
variable and avoiding direct freeing of iadev after
auxiliary_device_uninit().

Fixes: be91128c579c ("idpf: implement RDMA vport auxiliary dev create, init, and destroy")
Cc: stable@vger.kernel.org
Signed-off-by: Guangshuo Li <lgs201920130244@gmail.com>
---
v2:
  - note that the issue was identified by my static analysis tool
  - and confirmed by manual review

 drivers/net/ethernet/intel/idpf/idpf_idc.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/drivers/net/ethernet/intel/idpf/idpf_idc.c b/drivers/net/ethernet/intel/idpf/idpf_idc.c
index 6dad0593f7f2..2a18907643fc 100644
--- a/drivers/net/ethernet/intel/idpf/idpf_idc.c
+++ b/drivers/net/ethernet/intel/idpf/idpf_idc.c
@@ -59,6 +59,7 @@ static int idpf_plug_vport_aux_dev(struct iidc_rdma_core_dev_info *cdev_info,
 	char name[IDPF_IDC_MAX_ADEV_NAME_LEN];
 	struct auxiliary_device *adev;
 	int ret;
+	int adev_id;
 
 	iadev = kzalloc(sizeof(*iadev), GFP_KERNEL);
 	if (!iadev)
@@ -74,11 +75,14 @@ static int idpf_plug_vport_aux_dev(struct iidc_rdma_core_dev_info *cdev_info,
 		goto err_ida_alloc;
 	}
 	adev->id = ret;
+	adev->id = adev_id;
 	adev->dev.release = idpf_vport_adev_release;
 	adev->dev.parent = &cdev_info->pdev->dev;
 	sprintf(name, "%04x.rdma.vdev", cdev_info->pdev->vendor);
 	adev->name = name;
 
+	/* iadev is owned by the auxiliary device */
+	iadev = NULL;
 	ret = auxiliary_device_init(adev);
 	if (ret)
 		goto err_aux_dev_init;
@@ -92,7 +96,7 @@ static int idpf_plug_vport_aux_dev(struct iidc_rdma_core_dev_info *cdev_info,
 err_aux_dev_add:
 	auxiliary_device_uninit(adev);
 err_aux_dev_init:
-	ida_free(&idpf_idc_ida, adev->id);
+	ida_free(&idpf_idc_ida, adev_id);
 err_ida_alloc:
 	vdev_info->adev = NULL;
 	kfree(iadev);
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v2 net] net: ax25: fix integer overflow in ax25_rx_fragment()
From: Mashiro Chen @ 2026-04-13 11:21 UTC (permalink / raw)
  To: David Laight, Jakub Kicinski
  Cc: netdev, davem, edumazet, pabeni, horms, jreuter, linux-hams,
	linux-kernel, stable
In-Reply-To: <20260412220550.0f35f5ef@pumpkin>

Hi Jakub, Simon

v3 has addressed the review comments on v2:
1. Add pskb_may_pull(skb, 1) before dereferencing skb->data
2. Remove the unnecessary (unsigned int) cast on fraglen
3. Fix skb leak in overflow path that kfree_skb(skb) before return 1
4. Reset ax25->fraglen = 0 after purge


P.S.:
the reassembly copy loop at ax25_in.c:75 uses 
skb_copy_from_linear_data(skbo, dst, skbo->len), which is equivalent to 
memcpy(skbo->data, dst, skbo->len).
If a queued skbo contains non-linear data, which means data_len > 0, 
this silently reads only the linear head and copies stale data for the 
remainder.
In practice, all AX.25 lower-layer drivers like mkiss and 6pack allocate 
fully linear skbs via dev_alloc_skb(), so this is not currently 
reachable, I think there should be a separated patch to fix this.

73s,
Mashiro Chen

On 4/13/26 05:05, David Laight wrote:
> On Sun, 12 Apr 2026 13:17:51 -0700
> Jakub Kicinski <kuba@kernel.org> wrote:
>
>> On Thu,  9 Apr 2026 10:50:26 +0800 Mashiro Chen wrote:
>>> Fix mirrors the identical bug fixed in NET/ROM (nr_in.c): check for
>>> overflow before adding skb->len to fraglen, and abort fragment
>>> reassembly cleanly if the limit would be exceeded.
>> Same problem as reported by Simon on the netrom patch applies here.
>>
>> nit: I don't think you need to cast ax25->fraglen to unsigned int
>> in the comparison. since it's added with skb->len it should get
>> auto-prompted to unsigned int.
> It wouldn't matter if that comparison were signed.
>
> Or change the type of ax25->fraglen to be 32bits and do the
> sanity check for overlong packets later in the code.
> I had a quick look at the header and the structure hasn't
> been size-optimised...
>
> 	David
>

^ permalink raw reply

* [PATCH net-next 0/3] Follow-ups to nk_qlease net selftests
From: Daniel Borkmann @ 2026-04-13 11:40 UTC (permalink / raw)
  To: kuba; +Cc: pabeni, dw, razor, netdev

This is a set of follow-ups addressing [0]:

- Split netdevsim tests from HW tests in nk_qlease and move the SW
  tests under selftests/net/
- Remove multiple ksft_run()s to fix the recently enforced hard-fail
- Move all the setup inside the test cases for the ones under
  selftests/net/ (I'll defer the HW ones to David)
- Add more test coverage related to queue leasing behavior and corner
  cases, so now we have 45 tests in nk_qlease.py with netdevsim
  which does not need special HW

  [0] https://lore.kernel.org/netdev/20260409181950.7e099b6c@kernel.org

Daniel Borkmann (3):
  tools/ynl: Make YnlFamily closeable as a context manager
  selftests/net: Split netdevsim tests from HW tests in nk_qlease
  selftests/net: Add additional test coverage in nk_qlease

 tools/net/ynl/pyynl/lib/ynl.py                |   10 +
 .../selftests/drivers/net/hw/nk_qlease.py     | 1142 ---------
 tools/testing/selftests/net/Makefile          |    1 +
 tools/testing/selftests/net/nk_qlease.py      | 2097 +++++++++++++++++
 4 files changed, 2108 insertions(+), 1142 deletions(-)
 create mode 100755 tools/testing/selftests/net/nk_qlease.py

-- 
2.43.0


^ permalink raw reply

* [PATCH net-next 1/3] tools/ynl: Make YnlFamily closeable as a context manager
From: Daniel Borkmann @ 2026-04-13 11:40 UTC (permalink / raw)
  To: kuba; +Cc: pabeni, dw, razor, netdev
In-Reply-To: <20260413114011.588162-1-daniel@iogearbox.net>

YnlFamily opens an AF_NETLINK socket in __init__ but has no way
to release it other than leaving it to the GC. YnlFamily holds a
self reference cycle through SpecFamily's self.family = self
in its super().__init__() call, so refcount GC cannot reclaim
it and the socket stays open until the cyclic GC runs.

If a test creates a guest netns, instantiates a YnlFamily inside
it via NetNSEnter(), performs some test case work via Ynl, and
then deletes the netns, then the 'ip netns del' only drops the
mount binding and cleanup_net in the kernel never runs, so any
subsequent test case assertions that objects got cleaned up would
fail given this only gets triggered later via cyclic GC run.

Add an explicit close() that closes the netlink socket and wire
up the __enter__/__exit__ so callers can scope the instance
deterministically via 'with YnlFamily(...) as ynl: ...'.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/net/ynl/pyynl/lib/ynl.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 9c078599cea0..f63c6f828735 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -731,6 +731,16 @@ class YnlFamily(SpecFamily):
             bound_f = functools.partial(self._op, op_name)
             setattr(self, op.ident_name, bound_f)
 
+    def close(self):
+        if self.sock is not None:
+            self.sock.close()
+            self.sock = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc, tb):
+        self.close()
 
     def ntf_subscribe(self, mcast_name):
         mcast_id = self.nlproto.get_mcast_id(mcast_name, self.mcast_groups)
-- 
2.43.0


^ permalink raw reply related

* [PATCH net-next 2/3] selftests/net: Split netdevsim tests from HW tests in nk_qlease
From: Daniel Borkmann @ 2026-04-13 11:40 UTC (permalink / raw)
  To: kuba; +Cc: pabeni, dw, razor, netdev
In-Reply-To: <20260413114011.588162-1-daniel@iogearbox.net>

As pointed out in 3d2c3d2eea9a ("selftests: net: py: explicitly forbid
multiple ksft_run() calls"), ksft_run() cannot be called multiple times.

Move the netdevsim-based queue lease tests to selftests/net/ so that
each file has exactly one ksft_run() call.

The HW tests (io_uring ZC RX, queue attrs, XDP with MP, destroy) remain
in selftests/drivers/net/hw/.

Fixes: 65d657d80684 ("selftests/net: Add queue leasing tests with netkit")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/netdev/20260409181950.7e099b6c@kernel.org
---
 .../selftests/drivers/net/hw/nk_qlease.py     | 1142 ----------------
 tools/testing/selftests/net/Makefile          |    1 +
 tools/testing/selftests/net/nk_qlease.py      | 1168 +++++++++++++++++
 3 files changed, 1169 insertions(+), 1142 deletions(-)
 create mode 100755 tools/testing/selftests/net/nk_qlease.py

diff --git a/tools/testing/selftests/drivers/net/hw/nk_qlease.py b/tools/testing/selftests/drivers/net/hw/nk_qlease.py
index 2bc5ffe96c7d..aa83dc321328 100755
--- a/tools/testing/selftests/drivers/net/hw/nk_qlease.py
+++ b/tools/testing/selftests/drivers/net/hw/nk_qlease.py
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
 # SPDX-License-Identifier: GPL-2.0
 
-import errno
 import re
 import time
 import threading
@@ -10,23 +9,17 @@ from lib.py import (
     ksft_run,
     ksft_exit,
     ksft_eq,
-    ksft_ne,
     ksft_in,
     ksft_not_in,
     ksft_raises,
 )
 from lib.py import (
     NetDrvContEnv,
-    NetNS,
     NetNSEnter,
     EthtoolFamily,
     NetdevFamily,
-    RtnlFamily,
-    NetdevSimDev,
 )
 from lib.py import (
-    NlError,
-    Netlink,
     bkg,
     cmd,
     defer,
@@ -46,1100 +39,6 @@ def set_flow_rule(cfg):
     return int(values)
 
 
-def create_netkit(rxqueues):
-    all_links = ip("-d link show", json=True)
-    old_idxs = {
-        link["ifindex"]
-        for link in all_links
-        if link.get("linkinfo", {}).get("info_kind") == "netkit"
-    }
-
-    rtnl = RtnlFamily()
-    rtnl.newlink(
-        {
-            "linkinfo": {
-                "kind": "netkit",
-                "data": {
-                    "mode": "l2",
-                    "policy": "forward",
-                    "peer-policy": "forward",
-                },
-            },
-            "num-rx-queues": rxqueues,
-        },
-        flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
-    )
-
-    all_links = ip("-d link show", json=True)
-    nk_links = [
-        link
-        for link in all_links
-        if link.get("linkinfo", {}).get("info_kind") == "netkit"
-        and link["ifindex"] not in old_idxs
-    ]
-    nk_links.sort(key=lambda x: x["ifindex"])
-    return (
-        nk_links[1]["ifname"],
-        nk_links[1]["ifindex"],
-        nk_links[0]["ifname"],
-        nk_links[0]["ifindex"],
-    )
-
-
-def create_netkit_single(rxqueues):
-    rtnl = RtnlFamily()
-    rtnl.newlink(
-        {
-            "linkinfo": {
-                "kind": "netkit",
-                "data": {
-                    "mode": "l2",
-                    "pairing": "single",
-                },
-            },
-            "num-rx-queues": rxqueues,
-        },
-        flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
-    )
-
-    all_links = ip("-d link show", json=True)
-    nk_links = [
-        link
-        for link in all_links
-        if link.get("linkinfo", {}).get("info_kind") == "netkit"
-        and "UP" not in link.get("flags", [])
-    ]
-    return nk_links[0]["ifname"], nk_links[0]["ifindex"]
-
-
-def test_remove_phys(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        nk_queue_id = result["id"]
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["ifindex"], nk_guest_idx)
-    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
-
-    nsimdev.remove()
-    time.sleep(0.1)
-    ret = cmd(f"ip link show dev {nk_host}", fail=False)
-    ksft_ne(ret.ret, 0)
-
-
-def test_double_lease(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3)
-    defer(cmd, f"ip link del dev {nk_host}")
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        ksft_eq(result["id"], 1)
-
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": src_queue, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EBUSY)
-
-
-def test_virtual_lessor(netns) -> None:
-    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host_a}")
-    ip(f"link set dev {nk_host_a} up")
-    ip(f"link set dev {nk_guest_a} up")
-
-    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host_b}")
-
-    ip(f"link set dev {nk_guest_b} netns {netns.name}")
-    ip(f"link set dev {nk_host_b} up")
-    ip(f"link set dev {nk_guest_b} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_b_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nk_guest_a_idx,
-                        "queue": {"id": 0, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_phys_lessee(_netns) -> None:
-    nsimdev_a = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev_a.remove)
-    nsim_a = nsimdev_a.nsims[0]
-    ip(f"link set dev {nsim_a.ifname} up")
-
-    nsimdev_b = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev_b.remove)
-    nsim_b = nsimdev_b.nsims[0]
-    ip(f"link set dev {nsim_b.ifname} up")
-
-    netdevnl = NetdevFamily()
-    with ksft_raises(NlError) as e:
-        netdevnl.queue_create(
-            {
-                "ifindex": nsim_a.ifindex,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim_b.ifindex,
-                    "queue": {"id": 0, "type": "rx"},
-                },
-            }
-        )
-    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_different_lessors(netns) -> None:
-    nsimdev_a = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev_a.remove)
-    nsim_a = nsimdev_a.nsims[0]
-    ip(f"link set dev {nsim_a.ifname} up")
-
-    nsimdev_b = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev_b.remove)
-    nsim_b = nsimdev_b.nsims[0]
-    ip(f"link set dev {nsim_b.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim_a.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim_b.ifindex,
-                        "queue": {"id": 1, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
-
-
-def test_queue_out_of_range(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": 2, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.ERANGE)
-
-
-def test_resize_leased(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    ethnl = EthtoolFamily()
-    with ksft_raises(NlError) as e:
-        ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
-    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_self_lease(_netns) -> None:
-    nk_host, _, _, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    netdevnl = NetdevFamily()
-    with ksft_raises(NlError) as e:
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nk_guest_idx,
-                    "queue": {"id": 0, "type": "rx"},
-                },
-            }
-        )
-    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_veth_queue_create(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    ip("link add veth0 type veth peer name veth1")
-    defer(cmd, "ip link del dev veth0", fail=False)
-
-    all_links = ip("-d link show", json=True)
-    veth_peer = [
-        link
-        for link in all_links
-        if link.get("ifname") == "veth1"
-    ]
-    veth_peer_idx = veth_peer[0]["ifindex"]
-
-    ip(f"link set dev veth1 netns {netns.name}")
-    ip("link set dev veth0 up")
-    ip("link set dev veth1 up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": veth_peer_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": 1, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_create_tx_type(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "tx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": 1, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_create_primary(_netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, nk_host_idx, _, _ = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_host} up")
-
-    netdevnl = NetdevFamily()
-    with ksft_raises(NlError) as e:
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_host_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                },
-            }
-        )
-    ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
-
-
-def test_create_limit(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=1)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": 1, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_link_flap_phys(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}")
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        nk_queue_id = result["id"]
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
-
-    # Link flap the physical device
-    ip(f"link set dev {nsim.ifname} down")
-    ip(f"link set dev {nsim.ifname} up")
-
-    # Verify lease survives the flap
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
-
-
-def test_queue_get_virtual(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}")
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        nk_queue_id = result["id"]
-
-        # queue-get on virtual device's leased queue should not show lease
-        # info (lease info is only shown from the physical device's side)
-        queue_info = netdevnl.queue_get(
-            {"ifindex": nk_guest_idx, "id": nk_queue_id, "type": "rx"}
-        )
-        ksft_eq(queue_info["id"], nk_queue_id)
-        ksft_eq(queue_info["ifindex"], nk_guest_idx)
-        ksft_not_in("lease", queue_info)
-
-        # Default queue (not leased) also has no lease info
-        queue_info = netdevnl.queue_get(
-            {"ifindex": nk_guest_idx, "id": 0, "type": "rx"}
-        )
-        ksft_not_in("lease", queue_info)
-
-
-def test_remove_virt_first(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        ksft_eq(result["id"], 1)
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
-
-    # Delete netkit (virtual device removed first, physical stays)
-    cmd(f"ip link del dev {nk_host}")
-
-    # Verify lease is cleaned up on physical device
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_not_in("lease", queue_info)
-
-
-def test_multiple_leases(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=4)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        r1 = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        r2 = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 2, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    ksft_eq(r1["id"], 1)
-    ksft_eq(r2["id"], 2)
-
-    # Verify both leases visible on physical device
-    netdevnl = NetdevFamily()
-    q1 = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
-    )
-    q2 = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
-    )
-    ksft_in("lease", q1)
-    ksft_in("lease", q2)
-    ksft_eq(q1["lease"]["ifindex"], nk_guest_idx)
-    ksft_eq(q2["lease"]["ifindex"], nk_guest_idx)
-    ksft_eq(q1["lease"]["queue"]["id"], r1["id"])
-    ksft_eq(q2["lease"]["queue"]["id"], r2["id"])
-
-
-def test_lease_queue_tx_type(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": nsim.ifindex,
-                        "queue": {"id": 1, "type": "tx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-
-def test_invalid_netns(netns) -> None:
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": 1,
-                        "queue": {"id": 0, "type": "rx"},
-                        "netns-id": 999,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.ENONET)
-
-
-def test_invalid_phys_ifindex(netns) -> None:
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        with ksft_raises(NlError) as e:
-            netdevnl.queue_create(
-                {
-                    "ifindex": nk_guest_idx,
-                    "type": "rx",
-                    "lease": {
-                        "ifindex": 99999,
-                        "queue": {"id": 0, "type": "rx"},
-                        "netns-id": 0,
-                    },
-                }
-            )
-        ksft_eq(e.exception.nl_msg.error, -errno.ENODEV)
-
-
-def test_multi_netkit_remove_phys(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    # Create two netkit pairs, each leasing a different physical queue
-    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
-
-    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host_b}", fail=False)
-
-    ip(f"link set dev {nk_guest_a} netns {netns.name}")
-    ip(f"link set dev {nk_host_a} up")
-    ip(f"link set dev {nk_guest_a} up", ns=netns)
-
-    ip(f"link set dev {nk_guest_b} netns {netns.name}")
-    ip(f"link set dev {nk_host_b} up")
-    ip(f"link set dev {nk_guest_b} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_a_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_b_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 2, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    # Removing the physical device should take down both netkit pairs
-    nsimdev.remove()
-    time.sleep(0.1)
-    ret = cmd(f"ip link show dev {nk_host_a}", fail=False)
-    ksft_ne(ret.ret, 0)
-    ret = cmd(f"ip link show dev {nk_host_b}", fail=False)
-    ksft_ne(ret.ret, 0)
-
-
-def test_single_remove_phys(_netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_name, nk_idx = create_netkit_single(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_name}", fail=False)
-
-    ip(f"link set dev {nk_name} up")
-
-    netdevnl = NetdevFamily()
-    netdevnl.queue_create(
-        {
-            "ifindex": nk_idx,
-            "type": "rx",
-            "lease": {
-                "ifindex": nsim.ifindex,
-                "queue": {"id": 1, "type": "rx"},
-            },
-        }
-    )
-
-    # Removing the physical device should take down the single netkit device
-    nsimdev.remove()
-    time.sleep(0.1)
-    ret = cmd(f"ip link show dev {nk_name}", fail=False)
-    ksft_ne(ret.ret, 0)
-
-
-def test_link_flap_virt(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}")
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    src_queue = 1
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        nk_queue_id = result["id"]
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
-
-    # Link flap the virtual (netkit) device
-    ip(f"link set dev {nk_guest} down", ns=netns)
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    # Verify lease survives the virtual device flap
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
-
-
-def test_phys_queue_no_lease(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}")
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    # Physical queue 0 (not leased) should have no lease info
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 0, "type": "rx"}
-    )
-    ksft_not_in("lease", queue_info)
-
-    # Physical queue 1 (leased) should have lease info
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-
-
-def test_same_ns_lease(_netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_name, nk_idx = create_netkit_single(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_name}", fail=False)
-
-    ip(f"link set dev {nk_name} up")
-
-    netdevnl = NetdevFamily()
-    result = netdevnl.queue_create(
-        {
-            "ifindex": nk_idx,
-            "type": "rx",
-            "lease": {
-                "ifindex": nsim.ifindex,
-                "queue": {"id": 1, "type": "rx"},
-            },
-        }
-    )
-    ksft_eq(result["id"], 1)
-
-    # Same namespace: lease info should NOT have netns-id
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["ifindex"], nk_idx)
-    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
-    ksft_not_in("netns-id", queue_info["lease"])
-
-
-def test_resize_after_unlease(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 1, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    # Resize should fail while lease is active
-    ethnl = EthtoolFamily()
-    with ksft_raises(NlError) as e:
-        ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
-    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
-
-    # Delete netkit, clearing the lease
-    cmd(f"ip link del dev {nk_host}")
-
-    # Resize should now succeed
-    ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
-
-
-def test_lease_queue_zero(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": 0, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        ksft_eq(result["id"], 1)
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": 0, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
-
-
-def test_release_and_reuse(netns) -> None:
-    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
-    defer(nsimdev.remove)
-    nsim = nsimdev.nsims[0]
-    ip(f"link set dev {nsim.ifname} up")
-
-    src_queue = 1
-
-    # First lease
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-
-    # Delete netkit, freeing the lease
-    cmd(f"ip link del dev {nk_host}")
-
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_not_in("lease", queue_info)
-
-    # Re-create netkit and lease the same physical queue again
-    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
-    defer(cmd, f"ip link del dev {nk_host}", fail=False)
-
-    ip(f"link set dev {nk_guest} netns {netns.name}")
-    ip(f"link set dev {nk_host} up")
-    ip(f"link set dev {nk_guest} up", ns=netns)
-
-    with NetNSEnter(str(netns)):
-        netdevnl = NetdevFamily()
-        result = netdevnl.queue_create(
-            {
-                "ifindex": nk_guest_idx,
-                "type": "rx",
-                "lease": {
-                    "ifindex": nsim.ifindex,
-                    "queue": {"id": src_queue, "type": "rx"},
-                    "netns-id": 0,
-                },
-            }
-        )
-        ksft_eq(result["id"], 1)
-
-    netdevnl = NetdevFamily()
-    queue_info = netdevnl.queue_get(
-        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
-    )
-    ksft_in("lease", queue_info)
-    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
-
-
 def test_iou_zcrx(cfg) -> None:
     cfg.require_ipver("6")
     ethnl = EthtoolFamily()
@@ -1324,47 +223,6 @@ def test_destroy(cfg) -> None:
 
 
 def main() -> None:
-    netns = NetNS()
-    cmd("ip netns attach init 1")
-    ip("netns set init 0", ns=netns)
-    ip("link set lo up", ns=netns)
-
-    ksft_run(
-        [
-            test_remove_phys,
-            test_double_lease,
-            test_virtual_lessor,
-            test_phys_lessee,
-            test_different_lessors,
-            test_queue_out_of_range,
-            test_resize_leased,
-            test_self_lease,
-            test_create_tx_type,
-            test_create_primary,
-            test_create_limit,
-            test_link_flap_phys,
-            test_queue_get_virtual,
-            test_remove_virt_first,
-            test_multiple_leases,
-            test_lease_queue_tx_type,
-            test_invalid_netns,
-            test_invalid_phys_ifindex,
-            test_multi_netkit_remove_phys,
-            test_single_remove_phys,
-            test_link_flap_virt,
-            test_phys_queue_no_lease,
-            test_same_ns_lease,
-            test_resize_after_unlease,
-            test_lease_queue_zero,
-            test_release_and_reuse,
-            test_veth_queue_create,
-        ],
-        args=(netns,),
-    )
-
-    cmd("ip netns del init", fail=False)
-    del netns
-
     with NetDrvContEnv(__file__, rxqueues=2) as cfg:
         cfg.bin_local = path.abspath(
             path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx"
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 231245a95879..a275ed584026 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -65,6 +65,7 @@ TEST_PROGS := \
 	netdevice.sh \
 	netns-name.sh \
 	netns-sysctl.sh \
+	nk_qlease.py \
 	nl_netdev.py \
 	nl_nlctrl.py \
 	pmtu.sh \
diff --git a/tools/testing/selftests/net/nk_qlease.py b/tools/testing/selftests/net/nk_qlease.py
new file mode 100755
index 000000000000..6ed4fb5e90f6
--- /dev/null
+++ b/tools/testing/selftests/net/nk_qlease.py
@@ -0,0 +1,1168 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+import errno
+import time
+from lib.py import (
+    ksft_run,
+    ksft_exit,
+    ksft_eq,
+    ksft_ne,
+    ksft_in,
+    ksft_not_in,
+    ksft_raises,
+)
+from lib.py import (
+    NetNS,
+    NetNSEnter,
+    EthtoolFamily,
+    NetdevFamily,
+    RtnlFamily,
+    NetdevSimDev,
+)
+from lib.py import (
+    NlError,
+    Netlink,
+    cmd,
+    defer,
+    ip,
+)
+
+def create_netkit(rxqueues):
+    all_links = ip("-d link show", json=True)
+    old_idxs = {
+        link["ifindex"]
+        for link in all_links
+        if link.get("linkinfo", {}).get("info_kind") == "netkit"
+    }
+
+    rtnl = RtnlFamily()
+    rtnl.newlink(
+        {
+            "linkinfo": {
+                "kind": "netkit",
+                "data": {
+                    "mode": "l2",
+                    "policy": "forward",
+                    "peer-policy": "forward",
+                },
+            },
+            "num-rx-queues": rxqueues,
+        },
+        flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
+    )
+
+    all_links = ip("-d link show", json=True)
+    nk_links = [
+        link
+        for link in all_links
+        if link.get("linkinfo", {}).get("info_kind") == "netkit"
+        and link["ifindex"] not in old_idxs
+    ]
+    nk_links.sort(key=lambda x: x["ifindex"])
+    return (
+        nk_links[1]["ifname"],
+        nk_links[1]["ifindex"],
+        nk_links[0]["ifname"],
+        nk_links[0]["ifindex"],
+    )
+
+
+def create_netkit_single(rxqueues):
+    rtnl = RtnlFamily()
+    rtnl.newlink(
+        {
+            "linkinfo": {
+                "kind": "netkit",
+                "data": {
+                    "mode": "l2",
+                    "pairing": "single",
+                },
+            },
+            "num-rx-queues": rxqueues,
+        },
+        flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
+    )
+
+    all_links = ip("-d link show", json=True)
+    nk_links = [
+        link
+        for link in all_links
+        if link.get("linkinfo", {}).get("info_kind") == "netkit"
+        and "UP" not in link.get("flags", [])
+    ]
+    return nk_links[0]["ifname"], nk_links[0]["ifindex"]
+
+def test_remove_phys(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["ifindex"], nk_guest_idx)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+    nsimdev.remove()
+    time.sleep(0.1)
+    ret = cmd(f"ip link show dev {nk_host}", fail=False)
+    ksft_ne(ret.ret, 0)
+
+
+def test_double_lease(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3)
+    defer(cmd, f"ip link del dev {nk_host}")
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": src_queue, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EBUSY)
+
+
+def test_virtual_lessor(netns) -> None:
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up")
+
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_b}")
+
+    ip(f"link set dev {nk_guest_b} netns {netns.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_b_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nk_guest_a_idx,
+                        "queue": {"id": 0, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_phys_lessee(_netns) -> None:
+    nsimdev_a = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_a.remove)
+    nsim_a = nsimdev_a.nsims[0]
+    ip(f"link set dev {nsim_a.ifname} up")
+
+    nsimdev_b = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_b.remove)
+    nsim_b = nsimdev_b.nsims[0]
+    ip(f"link set dev {nsim_b.ifname} up")
+
+    netdevnl = NetdevFamily()
+    with ksft_raises(NlError) as e:
+        netdevnl.queue_create(
+            {
+                "ifindex": nsim_a.ifindex,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim_b.ifindex,
+                    "queue": {"id": 0, "type": "rx"},
+                },
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_different_lessors(netns) -> None:
+    nsimdev_a = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_a.remove)
+    nsim_a = nsimdev_a.nsims[0]
+    ip(f"link set dev {nsim_a.ifname} up")
+
+    nsimdev_b = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_b.remove)
+    nsim_b = nsimdev_b.nsims[0]
+    ip(f"link set dev {nsim_b.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim_a.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim_b.ifindex,
+                        "queue": {"id": 1, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+
+def test_queue_out_of_range(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 2, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.ERANGE)
+
+
+def test_resize_leased(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    ethnl = EthtoolFamily()
+    with ksft_raises(NlError) as e:
+        ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
+    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_self_lease(_netns) -> None:
+    nk_host, _, _, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    netdevnl = NetdevFamily()
+    with ksft_raises(NlError) as e:
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nk_guest_idx,
+                    "queue": {"id": 0, "type": "rx"},
+                },
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_veth_queue_create(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    ip("link add veth0 type veth peer name veth1")
+    defer(cmd, "ip link del dev veth0", fail=False)
+
+    all_links = ip("-d link show", json=True)
+    veth_peer = [
+        link
+        for link in all_links
+        if link.get("ifname") == "veth1"
+    ]
+    veth_peer_idx = veth_peer[0]["ifindex"]
+
+    ip(f"link set dev veth1 netns {netns.name}")
+    ip("link set dev veth0 up")
+    ip("link set dev veth1 up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": veth_peer_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 1, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_create_tx_type(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "tx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 1, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_create_primary(_netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, nk_host_idx, _, _ = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_host} up")
+
+    netdevnl = NetdevFamily()
+    with ksft_raises(NlError) as e:
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_host_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                },
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+
+def test_create_limit(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=1)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 1, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_link_flap_phys(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}")
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+    # Link flap the physical device
+    ip(f"link set dev {nsim.ifname} down")
+    ip(f"link set dev {nsim.ifname} up")
+
+    # Verify lease survives the flap
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+
+def test_queue_get_virtual(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}")
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+        # queue-get on virtual device's leased queue should not show lease
+        # info (lease info is only shown from the physical device's side)
+        queue_info = netdevnl.queue_get(
+            {"ifindex": nk_guest_idx, "id": nk_queue_id, "type": "rx"}
+        )
+        ksft_eq(queue_info["id"], nk_queue_id)
+        ksft_eq(queue_info["ifindex"], nk_guest_idx)
+        ksft_not_in("lease", queue_info)
+
+        # Default queue (not leased) also has no lease info
+        queue_info = netdevnl.queue_get(
+            {"ifindex": nk_guest_idx, "id": 0, "type": "rx"}
+        )
+        ksft_not_in("lease", queue_info)
+
+
+def test_remove_virt_first(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+
+    # Delete netkit (virtual device removed first, physical stays)
+    cmd(f"ip link del dev {nk_host}")
+
+    # Verify lease is cleaned up on physical device
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_not_in("lease", queue_info)
+
+
+def test_multiple_leases(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=4)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        r1 = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        r2 = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    ksft_eq(r1["id"], 1)
+    ksft_eq(r2["id"], 2)
+
+    # Verify both leases visible on physical device
+    netdevnl = NetdevFamily()
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_in("lease", q2)
+    ksft_eq(q1["lease"]["ifindex"], nk_guest_idx)
+    ksft_eq(q2["lease"]["ifindex"], nk_guest_idx)
+    ksft_eq(q1["lease"]["queue"]["id"], r1["id"])
+    ksft_eq(q2["lease"]["queue"]["id"], r2["id"])
+
+
+def test_lease_queue_tx_type(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 1, "type": "tx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_invalid_netns(netns) -> None:
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": 1,
+                        "queue": {"id": 0, "type": "rx"},
+                        "netns-id": 999,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.ENONET)
+
+
+def test_invalid_phys_ifindex(netns) -> None:
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        with ksft_raises(NlError) as e:
+            netdevnl.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": 99999,
+                        "queue": {"id": 0, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.ENODEV)
+
+
+def test_multi_netkit_remove_phys(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    # Create two netkit pairs, each leasing a different physical queue
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
+
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_b}", fail=False)
+
+    ip(f"link set dev {nk_guest_a} netns {netns.name}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up", ns=netns)
+
+    ip(f"link set dev {nk_guest_b} netns {netns.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_a_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_b_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    # Removing the physical device should take down both netkit pairs
+    nsimdev.remove()
+    time.sleep(0.1)
+    ret = cmd(f"ip link show dev {nk_host_a}", fail=False)
+    ksft_ne(ret.ret, 0)
+    ret = cmd(f"ip link show dev {nk_host_b}", fail=False)
+    ksft_ne(ret.ret, 0)
+
+
+def test_single_remove_phys(_netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_name, nk_idx = create_netkit_single(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_name}", fail=False)
+
+    ip(f"link set dev {nk_name} up")
+
+    netdevnl = NetdevFamily()
+    netdevnl.queue_create(
+        {
+            "ifindex": nk_idx,
+            "type": "rx",
+            "lease": {
+                "ifindex": nsim.ifindex,
+                "queue": {"id": 1, "type": "rx"},
+            },
+        }
+    )
+
+    # Removing the physical device should take down the single netkit device
+    nsimdev.remove()
+    time.sleep(0.1)
+    ret = cmd(f"ip link show dev {nk_name}", fail=False)
+    ksft_ne(ret.ret, 0)
+
+
+def test_link_flap_virt(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}")
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+    # Link flap the virtual (netkit) device
+    ip(f"link set dev {nk_guest} down", ns=netns)
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    # Verify lease survives the virtual device flap
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+
+def test_phys_queue_no_lease(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}")
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    # Physical queue 0 (not leased) should have no lease info
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 0, "type": "rx"}
+    )
+    ksft_not_in("lease", queue_info)
+
+    # Physical queue 1 (leased) should have lease info
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+
+def test_same_ns_lease(_netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_name, nk_idx = create_netkit_single(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_name}", fail=False)
+
+    ip(f"link set dev {nk_name} up")
+
+    netdevnl = NetdevFamily()
+    result = netdevnl.queue_create(
+        {
+            "ifindex": nk_idx,
+            "type": "rx",
+            "lease": {
+                "ifindex": nsim.ifindex,
+                "queue": {"id": 1, "type": "rx"},
+            },
+        }
+    )
+    ksft_eq(result["id"], 1)
+
+    # Same namespace: lease info should NOT have netns-id
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["ifindex"], nk_idx)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+    ksft_not_in("netns-id", queue_info["lease"])
+
+
+def test_resize_after_unlease(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    # Resize should fail while lease is active
+    ethnl = EthtoolFamily()
+    with ksft_raises(NlError) as e:
+        ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
+    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+    # Delete netkit, clearing the lease
+    cmd(f"ip link del dev {nk_host}")
+
+    # Resize should now succeed
+    ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1})
+
+
+def test_lease_queue_zero(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 0, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 0, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+
+
+def test_release_and_reuse(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    src_queue = 1
+
+    # First lease
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    # Delete netkit, freeing the lease
+    cmd(f"ip link del dev {nk_host}")
+
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_not_in("lease", queue_info)
+
+    # Re-create netkit and lease the same physical queue again
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)):
+        netdevnl = NetdevFamily()
+        result = netdevnl.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+
+
+def main() -> None:
+    netns = NetNS()
+    cmd("ip netns attach init 1")
+    ip("netns set init 0", ns=netns)
+    ip("link set lo up", ns=netns)
+
+    ksft_run(
+        [
+            test_remove_phys,
+            test_double_lease,
+            test_virtual_lessor,
+            test_phys_lessee,
+            test_different_lessors,
+            test_queue_out_of_range,
+            test_resize_leased,
+            test_self_lease,
+            test_create_tx_type,
+            test_create_primary,
+            test_create_limit,
+            test_link_flap_phys,
+            test_queue_get_virtual,
+            test_remove_virt_first,
+            test_multiple_leases,
+            test_lease_queue_tx_type,
+            test_invalid_netns,
+            test_invalid_phys_ifindex,
+            test_multi_netkit_remove_phys,
+            test_single_remove_phys,
+            test_link_flap_virt,
+            test_phys_queue_no_lease,
+            test_same_ns_lease,
+            test_resize_after_unlease,
+            test_lease_queue_zero,
+            test_release_and_reuse,
+            test_veth_queue_create,
+        ],
+        args=(netns,),
+    )
+
+    cmd("ip netns del init", fail=False)
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.43.0


^ permalink raw reply related

* [PATCH net-next 3/3] selftests/net: Add additional test coverage in nk_qlease
From: Daniel Borkmann @ 2026-04-13 11:40 UTC (permalink / raw)
  To: kuba; +Cc: pabeni, dw, razor, netdev
In-Reply-To: <20260413114011.588162-1-daniel@iogearbox.net>

Add further netkit queue-lease coverage for netns lifecycle of the guest
and physical halves, channel resize across active leases, single-device
and multi-lessee scenarios, L3 mode operation, lease capacity exhaustion,
and corner-cases of e.g. queue-create rejection paths.

Full test run:

  # ./nk_qlease.py
  TAP version 13
  1..45
  ok 1 nk_qlease.test_remove_phys
  ok 2 nk_qlease.test_double_lease
  ok 3 nk_qlease.test_virtual_lessor
  ok 4 nk_qlease.test_phys_lessee
  ok 5 nk_qlease.test_different_lessors
  ok 6 nk_qlease.test_queue_out_of_range
  ok 7 nk_qlease.test_resize_leased
  ok 8 nk_qlease.test_self_lease
  ok 9 nk_qlease.test_create_tx_type
  ok 10 nk_qlease.test_create_primary
  ok 11 nk_qlease.test_create_limit
  ok 12 nk_qlease.test_link_flap_phys
  ok 13 nk_qlease.test_queue_get_virtual
  ok 14 nk_qlease.test_remove_virt_first
  ok 15 nk_qlease.test_multiple_leases
  ok 16 nk_qlease.test_lease_queue_tx_type
  ok 17 nk_qlease.test_invalid_netns
  ok 18 nk_qlease.test_invalid_phys_ifindex
  ok 19 nk_qlease.test_multi_netkit_remove_phys
  ok 20 nk_qlease.test_single_remove_phys
  ok 21 nk_qlease.test_link_flap_virt
  ok 22 nk_qlease.test_phys_queue_no_lease
  ok 23 nk_qlease.test_same_ns_lease
  ok 24 nk_qlease.test_resize_after_unlease
  ok 25 nk_qlease.test_lease_queue_zero
  ok 26 nk_qlease.test_release_and_reuse
  ok 27 nk_qlease.test_veth_queue_create
  ok 28 nk_qlease.test_two_netkits_same_queue
  ok 29 nk_qlease.test_l3_mode_lease
  ok 30 nk_qlease.test_single_double_lease
  ok 31 nk_qlease.test_single_different_lessors
  ok 32 nk_qlease.test_cross_ns_netns_id
  ok 33 nk_qlease.test_delete_guest_netns
  ok 34 nk_qlease.test_move_guest_netns
  ok 35 nk_qlease.test_resize_phys_no_reduction
  ok 36 nk_qlease.test_delete_one_netkit_of_two
  ok 37 nk_qlease.test_bind_rx_leased_phys_queue
  ok 38 nk_qlease.test_resize_phys_shrink_past_leased
  ok 39 nk_qlease.test_resize_virt_not_supported
  ok 40 nk_qlease.test_lease_devices_down
  ok 41 nk_qlease.test_lease_capacity_exhaustion
  ok 42 nk_qlease.test_resize_phys_up
  ok 43 nk_qlease.test_multi_ns_lease
  ok 44 nk_qlease.test_multi_ns_delete_one
  ok 45 nk_qlease.test_move_phys_netns
  # Totals: pass:45 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/testing/selftests/net/nk_qlease.py | 933 ++++++++++++++++++++++-
 1 file changed, 931 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/net/nk_qlease.py b/tools/testing/selftests/net/nk_qlease.py
index 6ed4fb5e90f6..df35c82bccfc 100755
--- a/tools/testing/selftests/net/nk_qlease.py
+++ b/tools/testing/selftests/net/nk_qlease.py
@@ -28,7 +28,8 @@ from lib.py import (
     ip,
 )
 
-def create_netkit(rxqueues):
+
+def create_netkit(rxqueues, mode="l2"):
     all_links = ip("-d link show", json=True)
     old_idxs = {
         link["ifindex"]
@@ -42,7 +43,7 @@ def create_netkit(rxqueues):
             "linkinfo": {
                 "kind": "netkit",
                 "data": {
-                    "mode": "l2",
+                    "mode": mode,
                     "policy": "forward",
                     "peer-policy": "forward",
                 },
@@ -93,6 +94,7 @@ def create_netkit_single(rxqueues):
     ]
     return nk_links[0]["ifname"], nk_links[0]["ifindex"]
 
+
 def test_remove_phys(netns) -> None:
     nsimdev = NetdevSimDev(port_count=1, queue_count=2)
     defer(nsimdev.remove)
@@ -1121,6 +1123,915 @@ def test_release_and_reuse(netns) -> None:
     ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
 
 
+def test_two_netkits_same_queue(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
+
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_b}", fail=False)
+
+    ip(f"link set dev {nk_guest_a} netns {netns.name}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up", ns=netns)
+
+    ip(f"link set dev {nk_guest_b} netns {netns.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_a_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+        with ksft_raises(NlError) as e:
+            netdevnl_ns.queue_create(
+                {
+                    "ifindex": nk_guest_b_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": src_queue, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EBUSY)
+
+
+def test_l3_mode_lease(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2, mode="l3")
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        result = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["ifindex"], nk_guest_idx)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+
+
+def test_single_double_lease(_netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_name, nk_idx = create_netkit_single(rxqueues=3)
+    defer(cmd, f"ip link del dev {nk_name}", fail=False)
+
+    ip(f"link set dev {nk_name} up")
+
+    netdevnl = NetdevFamily()
+    result = netdevnl.queue_create(
+        {
+            "ifindex": nk_idx,
+            "type": "rx",
+            "lease": {
+                "ifindex": nsim.ifindex,
+                "queue": {"id": 1, "type": "rx"},
+            },
+        }
+    )
+    ksft_eq(result["id"], 1)
+
+    with ksft_raises(NlError) as e:
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                },
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EBUSY)
+
+
+def test_single_different_lessors(_netns) -> None:
+    nsimdev_a = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_a.remove)
+    nsim_a = nsimdev_a.nsims[0]
+    ip(f"link set dev {nsim_a.ifname} up")
+
+    nsimdev_b = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev_b.remove)
+    nsim_b = nsimdev_b.nsims[0]
+    ip(f"link set dev {nsim_b.ifname} up")
+
+    nk_name, nk_idx = create_netkit_single(rxqueues=3)
+    defer(cmd, f"ip link del dev {nk_name}", fail=False)
+
+    ip(f"link set dev {nk_name} up")
+
+    netdevnl = NetdevFamily()
+    netdevnl.queue_create(
+        {
+            "ifindex": nk_idx,
+            "type": "rx",
+            "lease": {
+                "ifindex": nsim_a.ifindex,
+                "queue": {"id": 1, "type": "rx"},
+            },
+        }
+    )
+
+    with ksft_raises(NlError) as e:
+        netdevnl.queue_create(
+            {
+                "ifindex": nk_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim_b.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                },
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+
+def test_cross_ns_netns_id(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_in("netns-id", queue_info["lease"])
+
+
+def test_delete_guest_netns(_netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    test_ns = NetNS()
+    ip("netns set init 0", ns=test_ns)
+    ip("link set lo up", ns=test_ns)
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+
+    ip(f"link set dev {nk_guest} netns {test_ns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=test_ns)
+
+    src_queue = 1
+    with NetNSEnter(str(test_ns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    del test_ns
+    time.sleep(0.1)
+
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_not_in("lease", queue_info)
+
+    ret = cmd(f"ip link show dev {nk_host}", fail=False)
+    ksft_ne(ret.ret, 0)
+
+
+def test_move_guest_netns(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        result = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+    new_ns = NetNS()
+    defer(new_ns.__del__)
+    ip(f"link set dev {nk_guest} netns {new_ns.name}", ns=netns)
+
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+
+def test_resize_phys_no_reduction(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    ethnl = EthtoolFamily()
+    ethnl.channels_set(
+        {"header": {"dev-index": nsim.ifindex}, "combined-count": 2}
+    )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+
+def test_delete_one_netkit_of_two(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
+
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_b}", fail=False)
+
+    ip(f"link set dev {nk_guest_a} netns {netns.name}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up", ns=netns)
+
+    ip(f"link set dev {nk_guest_b} netns {netns.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_a_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_b_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_in("lease", q2)
+
+    cmd(f"ip link del dev {nk_host_a}")
+
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_not_in("lease", q1)
+    ksft_in("lease", q2)
+
+
+def test_bind_rx_leased_phys_queue(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    with ksft_raises(NlError) as e:
+        netdevnl.bind_rx(
+            {
+                "ifindex": nsim.ifindex,
+                "fd": 0,
+                "queues": [
+                    {"id": 0, "type": "rx"},
+                    {"id": 1, "type": "rx"},
+                ],
+            }
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+
+def test_resize_phys_shrink_past_leased(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=4)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    ethnl = EthtoolFamily()
+
+    # Shrink past the leased queue — only queue 3 removed, queue 1 untouched
+    ethnl.channels_set(
+        {"header": {"dev-index": nsim.ifindex}, "combined-count": 3}
+    )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    # Shrink further — queue 2 removed, queue 1 still untouched
+    ethnl.channels_set(
+        {"header": {"dev-index": nsim.ifindex}, "combined-count": 2}
+    )
+
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    # Shrink into the leased queue — queue 1 is busy, must fail
+    with ksft_raises(NlError) as e:
+        ethnl.channels_set(
+            {"header": {"dev-index": nsim.ifindex}, "combined-count": 1}
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+
+def test_resize_virt_not_supported(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, nk_host_idx, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    # Channel resize on the netkit host must fail — not supported
+    ethnl = EthtoolFamily()
+    with ksft_raises(NlError) as e:
+        ethnl.channels_set(
+            {"header": {"dev-index": nk_host_idx}, "combined-count": 1}
+        )
+    ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+    # Lease must be intact
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+
+def test_lease_devices_down(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+
+    # Create lease while both physical and virtual devices are down
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        result = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(result["id"], 1)
+
+    # Bring devices up before queue_get: netdevsim only instantiates NAPIs in
+    # ndo_open, and netdev-genl queue_get returns -ENOENT without a NAPI.
+    ip(f"link set dev {nsim.ifname} up")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+    ksft_eq(queue_info["lease"]["queue"]["id"], result["id"])
+
+
+def test_lease_capacity_exhaustion(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=4)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    # rxqueues=3 means num_rx_queues=3, real_num_rx_queues starts at 1.
+    # Can create 2 leased queues (real goes 1->2->3) but not a 3rd (3->4 > 3).
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        r1 = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(r1["id"], 1)
+
+        r2 = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(r2["id"], 2)
+
+        # Third lease fails — netkit queue capacity exhausted
+        with ksft_raises(NlError) as e:
+            netdevnl_ns.queue_create(
+                {
+                    "ifindex": nk_guest_idx,
+                    "type": "rx",
+                    "lease": {
+                        "ifindex": nsim.ifindex,
+                        "queue": {"id": 3, "type": "rx"},
+                        "netns-id": 0,
+                    },
+                }
+            )
+        ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
+
+    # Verify the two successful leases are intact
+    netdevnl = NetdevFamily()
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_in("lease", q2)
+
+
+def test_resize_phys_up(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    # Shrink nsim first so we have room to grow
+    ethnl = EthtoolFamily()
+    ethnl.channels_set(
+        {"header": {"dev-index": nsim.ifindex}, "combined-count": 2}
+    )
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    # Grow channels — should succeed since leased queue is not removed
+    ethnl.channels_set(
+        {"header": {"dev-index": nsim.ifindex}, "combined-count": 3}
+    )
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    # New queue 2 should exist without a lease
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_not_in("lease", queue_info)
+
+
+def test_multi_ns_lease(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    ns_b = NetNS()
+    defer(ns_b.__del__)
+    ip("netns set init 0", ns=ns_b)
+    ip("link set lo up", ns=ns_b)
+
+    # First netkit pair, guest in netns
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
+    ip(f"link set dev {nk_guest_a} netns {netns.name}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up", ns=netns)
+
+    # Second netkit pair, guest in ns_b
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_b}", fail=False)
+    ip(f"link set dev {nk_guest_b} netns {ns_b.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=ns_b)
+
+    # Lease from netns
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        r_a = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_a_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(r_a["id"], 1)
+
+    # Lease from ns_b (different namespace, same physical device)
+    with NetNSEnter(str(ns_b)), NetdevFamily() as netdevnl_ns:
+        r_b = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_b_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        ksft_eq(r_b["id"], 1)
+
+    # Verify both leases from the physical side
+    netdevnl = NetdevFamily()
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_in("lease", q2)
+    ksft_eq(q1["lease"]["ifindex"], nk_guest_a_idx)
+    ksft_eq(q2["lease"]["ifindex"], nk_guest_b_idx)
+
+
+def test_multi_ns_delete_one(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=3)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    ns_b = NetNS()
+    ip("netns set init 0", ns=ns_b)
+    ip("link set lo up", ns=ns_b)
+
+    # First netkit pair, guest in netns (ns_a)
+    nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host_a}", fail=False)
+    ip(f"link set dev {nk_guest_a} netns {netns.name}")
+    ip(f"link set dev {nk_host_a} up")
+    ip(f"link set dev {nk_guest_a} up", ns=netns)
+
+    # Second netkit pair, guest in ns_b
+    nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2)
+
+    ip(f"link set dev {nk_guest_b} netns {ns_b.name}")
+    ip(f"link set dev {nk_host_b} up")
+    ip(f"link set dev {nk_guest_b} up", ns=ns_b)
+
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_a_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 1, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    with NetNSEnter(str(ns_b)), NetdevFamily() as netdevnl_ns:
+        netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_b_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": 2, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+
+    netdevnl = NetdevFamily()
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_in("lease", q2)
+
+    # Delete ns_b — destroys nk_guest_b, triggers unlease of queue 2
+    del ns_b
+    time.sleep(0.1)
+
+    # ns_a's lease on queue 1 must survive
+    q1 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 1, "type": "rx"}
+    )
+    ksft_in("lease", q1)
+    ksft_eq(q1["lease"]["ifindex"], nk_guest_a_idx)
+
+    # ns_b's lease on queue 2 must be gone
+    q2 = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": 2, "type": "rx"}
+    )
+    ksft_not_in("lease", q2)
+
+    # nk_host_b should be gone too (phys removal cascades to netkit pair)
+    ret = cmd(f"ip link show dev {nk_host_b}", fail=False)
+    ksft_ne(ret.ret, 0)
+
+
+def test_move_phys_netns(netns) -> None:
+    nsimdev = NetdevSimDev(port_count=1, queue_count=2)
+    defer(nsimdev.remove)
+    nsim = nsimdev.nsims[0]
+    ip(f"link set dev {nsim.ifname} up")
+
+    nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2)
+    defer(cmd, f"ip link del dev {nk_host}", fail=False)
+
+    ip(f"link set dev {nk_guest} netns {netns.name}")
+    ip(f"link set dev {nk_host} up")
+    ip(f"link set dev {nk_guest} up", ns=netns)
+
+    src_queue = 1
+    with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns:
+        result = netdevnl_ns.queue_create(
+            {
+                "ifindex": nk_guest_idx,
+                "type": "rx",
+                "lease": {
+                    "ifindex": nsim.ifindex,
+                    "queue": {"id": src_queue, "type": "rx"},
+                    "netns-id": 0,
+                },
+            }
+        )
+        nk_queue_id = result["id"]
+
+    netdevnl = NetdevFamily()
+    queue_info = netdevnl.queue_get(
+        {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"}
+    )
+    ksft_in("lease", queue_info)
+
+    # Move the physical device to a new namespace. Move it back to init_net
+    # on cleanup before the other defers fire (new_ns deletion, nsimdev.remove)
+    # so nsim lives in a stable namespace when they run.
+    new_ns = NetNS()
+    defer(new_ns.__del__)
+    ip(f"link set dev {nsim.ifname} netns {new_ns.name}")
+    defer(ip, f"link set dev {nsim.ifname} netns init", ns=new_ns)
+
+    # Physical device is now in new_ns — find its ifindex there
+    all_links = ip("-d link show", json=True, ns=new_ns)
+    nsim_in_new = [l for l in all_links if l.get("ifname") == nsim.ifname]
+    new_ifindex = nsim_in_new[0]["ifindex"]
+
+    # Moving a device across netns brings it admin-down; bring it back up so
+    # netdevsim re-creates the NAPI (netdev-genl queue_get needs it).
+    ip(f"link set dev {nsim.ifname} up", ns=new_ns)
+
+    # Verify lease survived the namespace move
+    with NetNSEnter(str(new_ns)), NetdevFamily() as netdevnl_ns:
+        queue_info = netdevnl_ns.queue_get(
+            {"ifindex": new_ifindex, "id": src_queue, "type": "rx"}
+        )
+        ksft_in("lease", queue_info)
+        ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id)
+
+
 def main() -> None:
     netns = NetNS()
     cmd("ip netns attach init 1")
@@ -1156,6 +2067,24 @@ def main() -> None:
             test_lease_queue_zero,
             test_release_and_reuse,
             test_veth_queue_create,
+            test_two_netkits_same_queue,
+            test_l3_mode_lease,
+            test_single_double_lease,
+            test_single_different_lessors,
+            test_cross_ns_netns_id,
+            test_delete_guest_netns,
+            test_move_guest_netns,
+            test_resize_phys_no_reduction,
+            test_delete_one_netkit_of_two,
+            test_bind_rx_leased_phys_queue,
+            test_resize_phys_shrink_past_leased,
+            test_resize_virt_not_supported,
+            test_lease_devices_down,
+            test_lease_capacity_exhaustion,
+            test_resize_phys_up,
+            test_multi_ns_lease,
+            test_multi_ns_delete_one,
+            test_move_phys_netns,
         ],
         args=(netns,),
     )
-- 
2.43.0


^ permalink raw reply related

* [PATCH net v1 2/2] selftests: fib_nexthops: test stale has_v4 on nexthop replace
From: Jiayuan Chen @ 2026-04-13 11:45 UTC (permalink / raw)
  To: netdev
  Cc: Jiayuan Chen, David Ahern, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan,
	linux-kernel, linux-kselftest
In-Reply-To: <20260413114522.147784-1-jiayuan.chen@linux.dev>

Add test cases that exercise the scenario where an IPv6 nexthop is
replaced with an IPv4 nexthop while being part of a group. The group's
has_v4 flag must be updated so that subsequent IPv6 route additions are
properly rejected.

Two cases are covered:
  1. Gateway nexthop replaced across families with an existing IPv6
     route on the group (rejected by fib6_check_nh_list).
  2. Blackhole nexthop replaced across families with no existing IPv6
     route on the group (fib6_check_nh_list returns early) — this is
     the path that triggers a NULL ptr deref without the kernel fix.

Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
 tools/testing/selftests/net/fib_nexthops.sh | 22 +++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tools/testing/selftests/net/fib_nexthops.sh b/tools/testing/selftests/net/fib_nexthops.sh
index 6eb7f95e70e1..ac868a731694 100755
--- a/tools/testing/selftests/net/fib_nexthops.sh
+++ b/tools/testing/selftests/net/fib_nexthops.sh
@@ -1209,6 +1209,28 @@ ipv6_fcnal_runtime()
 	run_cmd "$IP ro replace 2001:db8:101::1/128 nhid 124"
 	log_test $? 0 "IPv6 route using a group after replacing v4 gateways"
 
+	# Replacing an IPv6 nexthop with an IPv4 nexthop should update has_v4
+	# for all groups using it, preventing IPv6 routes from referencing the
+	# group after the replace.
+	run_cmd "$IP nexthop add id 89 via 2001:db8:91::2 dev veth1"
+	run_cmd "$IP nexthop add id 125 group 89"
+	run_cmd "$IP nexthop replace id 89 via 172.16.1.1 dev veth1"
+	run_cmd "$IP ro replace 2001:db8:101::1/128 nhid 125"
+	log_test $? 2 "IPv6 route can not use group after v6 nexthop replaced by v4"
+
+	# Same scenario but with a blackhole nexthop: the group has no IPv6
+	# routes yet when the replace happens, so fib6_check_nh_list returns
+	# early without checking. has_v4 must still be updated to block
+	# subsequent IPv6 route additions.
+	run_cmd "$IP nexthop flush >/dev/null 2>&1"
+	run_cmd "$IP -6 nexthop add id 90 blackhole"
+	run_cmd "$IP nexthop add id 125 group 90"
+	run_cmd "$IP nexthop replace id 90 blackhole"
+	run_cmd "$IP -6 ro add 2001:db8:101::1/128 nhid 125"
+	log_test $? 2 "IPv6 route reject v6 blackhole replaced by v4 blackhole"
+	run_cmd "ip netns exec $me ping -6 2001:db8:101::1 -c1 -w$PING_TIMEOUT"
+	log_test $? 2 "Ping unreachable after rejected route"
+
 	$IP nexthop flush >/dev/null 2>&1
 
 	#
-- 
2.43.0


^ permalink raw reply related

* [PATCH net v1 1/2] nexthop: fix IPv6 route referencing IPv4 nexthop
From: Jiayuan Chen @ 2026-04-13 11:45 UTC (permalink / raw)
  To: netdev
  Cc: Jiayuan Chen, David Ahern, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan,
	linux-kernel, linux-kselftest

syzbot reported a panic [1] [2].

When an IPv6 nexthop is replaced with an IPv4 nexthop, the has_v4 flag
of all groups containing this nexthop is not updated. This is because
nh_group_v4_update is only called when replacing AF_INET to AF_INET6,
but the reverse direction (AF_INET6 to AF_INET) is missed.

This allows a stale has_v4=false to bypass fib6_check_nexthop, causing
IPv6 routes to be attached to groups that effectively contain only AF_INET
members. Subsequent route lookups then call nexthop_fib6_nh() which
returns NULL for the AF_INET member, leading to a NULL pointer
dereference.

Fix by calling nh_group_v4_update whenever the family changes, not just
AF_INET to AF_INET6.

Reproducer:
	# AF_INET6 blackhole
	ip -6 nexthop add id 1 blackhole
	# group with has_v4=false
	ip nexthop add id 100 group 1
	# replace with AF_INET (no -6), has_v4 stays false
	ip nexthop replace id 1 blackhole
	# pass stale has_v4 check
	ip -6 route add 2001:db8::/64 nhid 100
	# panic
	ping -6 2001:db8::1

[1] https://syzkaller.appspot.com/bug?id=e17283eb2f8dcf3dd9b47fe6f67a95f71faadad0
[2] https://syzkaller.appspot.com/bug?id=8699b6ae54c9f35837d925686208402949e12ef3
Fixes: 7bf4796dd099 ("nexthops: add support for replace")
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
 net/ipv4/nexthop.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/net/ipv4/nexthop.c b/net/ipv4/nexthop.c
index 2c9036c719b6..11a763cbc848 100644
--- a/net/ipv4/nexthop.c
+++ b/net/ipv4/nexthop.c
@@ -2466,10 +2466,10 @@ static int replace_nexthop_single(struct net *net, struct nexthop *old,
 			goto err_notify;
 	}
 
-	/* When replacing an IPv4 nexthop with an IPv6 nexthop, potentially
+	/* When replacing a nexthop with one of a different family, potentially
 	 * update IPv4 indication in all the groups using the nexthop.
 	 */
-	if (oldi->family == AF_INET && newi->family == AF_INET6) {
+	if (oldi->family != newi->family) {
 		list_for_each_entry(nhge, &old->grp_list, nh_list) {
 			struct nexthop *nhp = nhge->nh_parent;
 			struct nh_group *nhg;
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH iwl-net 2/5] iavf: fix error path in iavf_request_misc_irq
From: Przemek Kitszel @ 2026-04-13 11:53 UTC (permalink / raw)
  To: Aleksandr Loktionov; +Cc: netdev, intel-wired-lan, anthony.l.nguyen
In-Reply-To: <20260413073035.4082204-3-aleksandr.loktionov@intel.com>

On 4/13/26 09:30, Aleksandr Loktionov wrote:
> From: Piotr Gardocki <piotrx.gardocki@intel.com>
> 
> When request_irq() fails the interrupt vector was not registered for
> the driver. Calling free_irq() on a vector that was never successfully
> requested triggers a kernel warning. Drop the erroneous free_irq()
> call from the error path.
> 
> Fixes: 5eae00c57f5e ("i40evf: main driver core")
> Signed-off-by: Piotr Gardocki <piotrx.gardocki@intel.com>
> Signed-off-by: Aleksandr Loktionov <aleksandr.loktionov@intel.com>
> ---
>   drivers/net/ethernet/intel/iavf/iavf_main.c | 1 -
>   1 file changed, 1 deletion(-)
> 
> diff --git a/drivers/net/ethernet/intel/iavf/iavf_main.c b/drivers/net/ethernet/intel/iavf/iavf_main.c
> index dad001a..ab5f5adc 100644
> --- a/drivers/net/ethernet/intel/iavf/iavf_main.c
> +++ b/drivers/net/ethernet/intel/iavf/iavf_main.c
> @@ -587,7 +587,6 @@ static int iavf_request_misc_irq(struct iavf_adapter *adapter)
>   		dev_err(&adapter->pdev->dev,
>   			"request_irq for %s failed: %d\n",
>   			adapter->misc_vector_name, err);
> -		free_irq(adapter->msix_entries[0].vector, netdev);
>   	}
>   	return err;
>   }

Reviewed-by: Przemek Kitszel <przemyslaw.kitszel@intel.com>

next time please CC netdev on IWL submissions

^ permalink raw reply

* Re: [PATCH 2/4] tools: ynl-gen-c: optionally emit structs and helpers
From: Christoph Böhmwalder @ 2026-04-13 11:48 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: Jens Axboe, drbd-dev, linux-kernel, Lars Ellenberg,
	Philipp Reisner, linux-block, Donald Hunter, Eric Dumazet, netdev
In-Reply-To: <20260412125502.3f8ff576@kernel.org>

On Sun, Apr 12, 2026 at 12:55:02PM -0700, Jakub Kicinski wrote:
>On Tue,  7 Apr 2026 19:33:54 +0200 Christoph Böhmwalder wrote:
>> The new flags in the genetlink-legacy spec that are required for
>> existing consumers to keep working are:
>>
>>   "default": a literal value or C define that sets the default value
>>   for an attribute, consumed by set_defaults().
>>
>>   "required": if true, from_attrs() returns an error when this
>>   attribute is missing from the request message.
>>
>>   "nla-policy-type": can be used to override the NLA type used in
>>   policy arrays. This is needed when the semantic type differs from
>>   the wire type for backward compatibility: genl_magic maps s32 fields
>>   to NLA_U32/nla_get_u32, and existing userspace might depend on this
>>   encoding. The immediate motivation is DRBD, whose genl spec
>>   definition predates the addition of signed types in genl. However,
>>   this is a generic issue that potentially affects multiple families:
>>   for example, nftables has NFTA_HOOK_PRIORITY as s32 in the spec but
>>   NLA_U32 in the actual kernel policy.
>
>The series doesn't apply for me (neither to Linus's tree nor
>to networking trees), so I didn't experiment with this code.

It's based on for-7.1/block in Jens' tree because there are some
prerequisite commits on there that haven't made it to master yet.

If required, I can also send the net-specific patches based on another
tree, just thought it made sense to keep it all together to have the
whole context in one place.

>Are the new code gen additions purely for the kernel?

Yes. The DRBD userspace utilities re-use the kernel headers and manually
construct messages using libgenl, so we can just do the same for the
legacy family.

>Can we just commit the code they output and leave the YNL itself be?
>Every single legacy family has some weird quirks the point of YNL
>is to get rid of them, not support them all..

Fair enough, we could also do that. Though the question then becomes
whether we want to keep the YAML spec for the "drbd" family (patch 3 of
this series) in Documentation/.

I would argue it makes sense to keep it around somewhere so that the old
family is somehow documented, but obviously that yaml file won't work
with the unmodified generator.

Maybe keep it, but with a comment at the top that notes that
- this family is deprecated and "frozen",
- the spec is only for documentation purposes, and
- the spec doesn't work with the upstream parser?

Thoughts?

^ permalink raw reply

* Re: commit 0c4f1c02d27a880b cause a deadlock issue
From: Thorsten Leemhuis @ 2026-04-13 11:58 UTC (permalink / raw)
  To: Greg KH
  Cc: He, Guocai (CN), Berg, Johannes, Friend,
	Linux kernel regressions list, Korenblit, Miriam Rachel,
	stable@vger.kernel.org
In-Reply-To: <DM3PPF63A6024A9E931C940F849C60FAF9EA35EA@DM3PPF63A6024A9.namprd11.prod.outlook.com>

On 4/3/26 15:00, Korenblit, Miriam Rachel wrote:
>> From: Greg KH <gregkh@linuxfoundation.org>
>> On Fri, Apr 03, 2026 at 12:44:48PM +0000, Korenblit, Miriam Rachel wrote:
>>>> -----Original Message-----
>>>> From: Greg KH <gregkh@linuxfoundation.org>
>>>> On Fri, Apr 03, 2026 at 11:08:46AM +0000, He, Guocai (CN) wrote:
>>>>> No, The mainline have no this issue.
>>>>> The changes of 0c4f1c02d27a880b is not in mainline.
>>>>
>>>> That does not make sense, that commit is really commit e1696c8bd005
>>>> ("wifi: cfg80211: stop NAN and P2P in cfg80211_leave") which is in
>>>> all of the following releases:
>>>> 	5.10.252 5.15.202 6.1.165 6.6.128 6.12.75 6.18.14 6.19.4 7.0-rc1
>>>> confused,
>>> The change is indeed in mainline, but the locking situation in
>>> mainline is totally different (that mutex does not even exist there)
>>> Therefore, the issue is not supposed to happen in mainline.
>>
>> Ok, does that commit now need to be reverted from some of the stable branches?
>> If so, which ones?
> 
> From every version which is < 6.7.

Greg, do you still have this in your todo mail queue somewhere? Just
wondering, as last weeks 6.6.y released afics lacked a revert of
e1696c8bd0056b ("wifi: cfg80211: stop NAN and P2P in cfg80211_leave") --
and I cannot spot one in your public stable queue either.

These are the commits that according to Miri need to be reverted if I
understood things right:

v6.6.128 (4d7a05da767e5c), v6.1.165 (0c4f1c02d27a88), v5.15.202
(31344ffecd7a34), v5.10.252 (d91240f24e831d)

Caio, Thorsten

^ permalink raw reply

* Re: [PATCH v5.15-v6.1] netfilter: nft_set_pipapo: do not rely on ZERO_SIZE_PTR
From: Greg KH @ 2026-04-13 11:59 UTC (permalink / raw)
  To: Keerthana K
  Cc: stable, pablo, kadlec, fw, davem, edumazet, kuba, pabeni,
	netfilter-devel, coreteam, netdev, linux-kernel, ajay.kaher,
	alexey.makhalov, vamsi-krishna.brahmajosyula, yin.ding,
	tapas.kundu, Stefano Brivio, Mukul Sikka, Brennan Lamoreaux
In-Reply-To: <20260413043247.3327855-1-keerthana.kalyanasundaram@broadcom.com>

On Mon, Apr 13, 2026 at 04:32:47AM +0000, Keerthana K wrote:
> From: Florian Westphal <fw@strlen.de>
> 
> commit 07ace0bbe03b3d8e85869af1dec5e4087b1d57b8 upstream
> 
> pipapo relies on kmalloc(0) returning ZERO_SIZE_PTR (i.e., not NULL
> but pointer is invalid).
> 
> Rework this to not call slab allocator when we'd request a 0-byte
> allocation.
> 
> Reviewed-by: Stefano Brivio <sbrivio@redhat.com>
> Signed-off-by: Florian Westphal <fw@strlen.de>
> Signed-off-by: Mukul Sikka <mukul.sikka@broadcom.com>
> Signed-off-by: Brennan Lamoreaux <brennan.lamoreaux@broadcom.com>
> [Keerthana: In older stable branches (v6.6 and earlier), the allocation logic in
> pipapo_clone() still relies on `src->rules` rather than `src->rules_alloc`
> (introduced in v6.9 via 9f439bd6ef4f). Consequently, the previously
> backported INT_MAX clamping check uses `src->rules`. This patch correctly
> moves that `src->rules > (INT_MAX / ...)` check inside the new
> `if (src->rules > 0)` block]
> Signed-off-by: Keerthana K <keerthana.kalyanasundaram@broadcom.com>
> ---
>  net/netfilter/nft_set_pipapo.c | 20 ++++++++++++++------
>  1 file changed, 14 insertions(+), 6 deletions(-)

Does not apply to 5.15.y :(

^ 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