Netdev List
 help / color / mirror / Atom feed
* [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

* Re: [RFC] Proposal: Add sysfs interface for PCIe TPH Steering Tag retrieval and configuration
From: fengchengwen @ 2026-04-13 12:04 UTC (permalink / raw)
  To: Leon Romanovsky
  Cc: Jason Gunthorpe, Bjorn Helgaas, linux-rdma, linux-pci, netdev,
	dri-devel, Keith Busch, Yochai Cohen, Yishai Hadas, Zhiping Zhang
In-Reply-To: <20260413100152.GG21470@unreal>

On 4/13/2026 6:01 PM, Leon Romanovsky wrote:
> On Fri, Apr 10, 2026 at 10:30:52PM +0800, fengchengwen wrote:
>> Hi all,
>>
>> I'm writing to propose adding a sysfs interface to expose and configure the
>> PCIe TPH
>> Steering Tag for PCIe devices, which is retrieved inside the kernel.
>>
>>
>> Background: The TPH Steering Tag is tightly coupled with both a PCIe device
>> (identified
>> by its BDF) and a CPU core. It can only be obtained in kernel mode. To allow
>> user-space
>> applications to fetch and set this value securely and conveniently, we need
>> a standard
>> kernel-to-user interface.
>>
>>
>> Proposed Solution: Add several sysfs attributes under each PCIe device's
>> sysfs directory:
>> 1. /sys/bus/pci/devices/<BDF>/tph_mode to query the TPH mode (interrupt or
>> device specific)
>> 2. /sys/bus/pci/devices/<BDF>/tph_enable to control the TPH feature
>> 3. /sys/bus/pci/devices/<BDF>/tph_st to support both read and write
>> operations, e.g.:
>>    Read operation:
>>      echo "cpu=3" > /sys/bus/pci/devices/0000:01:00.0/tph_st
>>      cat /sys/bus/pci/devices/0000:01:00.0/tph_st
>>    Write operation:
>>      echo "index=10 st=123" > /sys/bus/pci/devices/0000:01:00.0/tph_st
>>
>>
>> The design strictly follows PCI subsystem sysfs standards and has the
>> following key properties:
>>
>> 1. Dynamic Visibility: The sysfs attributes will only be present for PCIe
>> devices that
>>    support TPH Steering Tag. Devices without TPH capability will not show
>> these nodes,
>>    avoiding unnecessary user confusion.
>>
>> 2. Permission Control: The attributes will use 0600 file permissions,
>> ensuring only
>>    privileged root users can read or write them, which satisfies security
>> requirements
>>    for hardware configuration interfaces.
>>
>> 3. Standard Implementation Location: The interface will be implemented in
>>    drivers/pci/pci-sysfs.c, the canonical location for all PCI device sysfs
>> attributes,
>>    ensuring consistency and maintainability within the PCI subsystem.
>>
>>
>> Why sysfs instead of alternatives like VFIO-PCI ioctl:
>>
>> - Universality: sysfs does not require binding the device to a special
>> driver such as
>>   vfio-pci. It is available to any privileged user-space component,
>> including system
>>   utilities, daemons, and monitoring tools.
>>
>> - Simplicity: Both user-space usage (cat/echo) and kernel implementation are
>>   straightforward, reducing code complexity and long-term maintenance cost.
>>
>> - Design Alignment: TPH Steering Tag is a generic PCIe device feature, not
>> specific to
>>   user-space drivers like DPDK or VFIO. Exposing it via sysfs matches the
>> kernel's
>>   standard pattern for hardware capabilities.
>>
>>
>> I look forward to your comments about this design before submitting the
>> final patch.
> 
> You need to explain more clearly why this write functionality is useful
> and necessary outside the VFIO/RDMA context:
> https://lore.kernel.org/all/20260324234615.3731237-1-zhipingz@meta.com/
> 
> AFAIK, for non-VFIO TPH callers, kernel has enough knowledge to set
> right ST values.
> 
> There are several comments regarding the implementation, but those can wait
> until the rationale behind the proposal is fully clarified.

Thanks for your review and comments.

Let me clarify the rationale behind this user-space sysfs interface:

1. VFIO is just one of the user-space device access frameworks.
   There are many other in-kernel frameworks that expose devices
   to user space, such as UIO, UACCE, etc., which may also require
   TPH Steering Tag support.

2. The kernel can automatically program Steering Tags only when
   the device provides a standard ST table in MSI-X or config space.
   However, many devices implement vendor-specific or platform-specific
   Steering Tag programming methods that cannot be fully handled
   by the generic kernel code.

3. For such devices, user-space applications or framework drivers
   need to retrieve and configure TPH Steering Tags directly.
   A unified sysfs interface allows all user-space frameworks
   (not just VFIO) to use a common, standard way to manage
   TPH Steering Tags, rather than implementing duplicated logic
   in each subsystem.

This interface provides a uniform method for any user-space
device access solution to work with TPH, which is why I believe
it is useful and necessary beyond the VFIO/RDMA case.

Thanks

> 
> Thanks
> 
>>
>> Best regards,
>> Chengwen Feng
>>


^ permalink raw reply

* Re: [PATCH v3 net-next 13/15] net/sched: sch_cake: annotate data-races in cake_dump_stats()
From: Toke Høiland-Jørgensen @ 2026-04-13 12:07 UTC (permalink / raw)
  To: Eric Dumazet, David S . Miller, Jakub Kicinski, Paolo Abeni
  Cc: Simon Horman, Jamal Hadi Salim, Jiri Pirko, netdev, eric.dumazet,
	Eric Dumazet
In-Reply-To: <20260410182257.774311-14-edumazet@google.com>

Eric Dumazet <edumazet@google.com> writes:

> cake_dump_stats() and cake_dump_class_stats() run without qdisc
> spinlock being held.
>
> Add READ_ONCE()/WRITE_ONCE() annotations.
>
> Fixes: 046f6fd5daef ("sched: Add Common Applications Kept Enhanced (cake) qdisc")
> Signed-off-by: Eric Dumazet <edumazet@google.com>
> Cc: "Toke Høiland-Jørgensen" <toke@toke.dk>
> ---
>  net/sched/sch_cake.c | 404 ++++++++++++++++++++++++-------------------
>  1 file changed, 225 insertions(+), 179 deletions(-)

One of these diffstats is not like the others - thanks for tackling this :)

A few nits below:

> diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
> index 32e672820c00a88c6d8fe77a6308405e016525ea..f523f0aa4d830e9d3ec4d43bb123e1dc4f8f289d 100644
> --- a/net/sched/sch_cake.c
> +++ b/net/sched/sch_cake.c
> @@ -399,14 +399,14 @@ static void cake_configure_rates(struct Qdisc *sch, u64 rate, bool rate_adjust);
>   * Here, invsqrt is a fixed point number (< 1.0), 32bit mantissa, aka Q0.32
>   */
>  
> -static void cobalt_newton_step(struct cobalt_vars *vars)
> +static void cobalt_newton_step(struct cobalt_vars *vars, u32 count)
>  {
>  	u32 invsqrt, invsqrt2;
>  	u64 val;
>  
>  	invsqrt = vars->rec_inv_sqrt;
>  	invsqrt2 = ((u64)invsqrt * invsqrt) >> 32;
> -	val = (3LL << 32) - ((u64)vars->count * invsqrt2);
> +	val = (3LL << 32) - ((u64)count * invsqrt2);
>  
>  	val >>= 2; /* avoid overflow in following multiply */
>  	val = (val * invsqrt) >> (32 - 2 + 1);
> @@ -414,12 +414,12 @@ static void cobalt_newton_step(struct cobalt_vars *vars)
>  	vars->rec_inv_sqrt = val;
>  }
>  
> -static void cobalt_invsqrt(struct cobalt_vars *vars)
> +static void cobalt_invsqrt(struct cobalt_vars *vars, u32 count)
>  {
> -	if (vars->count < REC_INV_SQRT_CACHE)
> -		vars->rec_inv_sqrt = inv_sqrt_cache[vars->count];
> +	if (count < REC_INV_SQRT_CACHE)
> +		vars->rec_inv_sqrt = inv_sqrt_cache[count];
>  	else
> -		cobalt_newton_step(vars);
> +		cobalt_newton_step(vars, count);
>  }
>  
>  static void cobalt_vars_init(struct cobalt_vars *vars)
> @@ -449,16 +449,19 @@ static bool cobalt_queue_full(struct cobalt_vars *vars,
>  	bool up = false;
>  
>  	if (ktime_to_ns(ktime_sub(now, vars->blue_timer)) > p->target) {
> -		up = !vars->p_drop;
> -		vars->p_drop += p->p_inc;
> -		if (vars->p_drop < p->p_inc)
> -			vars->p_drop = ~0;
> -		vars->blue_timer = now;
> -	}
> -	vars->dropping = true;
> -	vars->drop_next = now;
> +		u32 p_drop = vars->p_drop;
> +
> +		up = !p_drop;
> +		p_drop += p->p_inc;
> +		if (p_drop < p->p_inc)
> +			p_drop = ~0;
> +		WRITE_ONCE(vars->p_drop, p_drop);
> +		WRITE_ONCE(vars->blue_timer, now);
> +	}
> +	WRITE_ONCE(vars->dropping, true);
> +	WRITE_ONCE(vars->drop_next, now);
>  	if (!vars->count)
> -		vars->count = 1;
> +		WRITE_ONCE(vars->count, 1);
>  
>  	return up;
>  }
> @@ -474,21 +477,25 @@ static bool cobalt_queue_empty(struct cobalt_vars *vars,
>  
>  	if (vars->p_drop &&
>  	    ktime_to_ns(ktime_sub(now, vars->blue_timer)) > p->target) {
> -		if (vars->p_drop < p->p_dec)
> -			vars->p_drop = 0;
> +		u32 p_drop = vars->p_drop;
> +
> +		if (p_drop < p->p_dec)
> +			p_drop = 0;
>  		else
> -			vars->p_drop -= p->p_dec;
> -		vars->blue_timer = now;
> -		down = !vars->p_drop;
> +			p_drop -= p->p_dec;
> +		WRITE_ONCE(vars->p_drop, p_drop);
> +		WRITE_ONCE(vars->blue_timer, now);
> +		down = !p_drop;
>  	}
> -	vars->dropping = false;
> +	WRITE_ONCE(vars->dropping, false);
>  
>  	if (vars->count && ktime_to_ns(ktime_sub(now, vars->drop_next)) >= 0) {
> -		vars->count--;
> -		cobalt_invsqrt(vars);
> -		vars->drop_next = cobalt_control(vars->drop_next,
> -						 p->interval,
> -						 vars->rec_inv_sqrt);
> +		WRITE_ONCE(vars->count, vars->count - 1);
> +		cobalt_invsqrt(vars, vars->count);
> +		WRITE_ONCE(vars->drop_next,
> +			   cobalt_control(vars->drop_next,
> +					  p->interval,
> +					  vars->rec_inv_sqrt));
>  	}
>  
>  	return down;
> @@ -507,6 +514,7 @@ static enum qdisc_drop_reason cobalt_should_drop(struct cobalt_vars *vars,
>  	bool next_due, over_target;
>  	ktime_t schedule;
>  	u64 sojourn;
> +	u32 count;
>  
>  /* The 'schedule' variable records, in its sign, whether 'now' is before or
>   * after 'drop_next'.  This allows 'drop_next' to be updated before the next
> @@ -528,45 +536,50 @@ static enum qdisc_drop_reason cobalt_should_drop(struct cobalt_vars *vars,
>  	over_target = sojourn > p->target &&
>  		      sojourn > p->mtu_time * bulk_flows * 2 &&
>  		      sojourn > p->mtu_time * 4;
> -	next_due = vars->count && ktime_to_ns(schedule) >= 0;
> +	count = vars->count;
> +	next_due = count && ktime_to_ns(schedule) >= 0;
>  
>  	vars->ecn_marked = false;
>  
>  	if (over_target) {
>  		if (!vars->dropping) {
> -			vars->dropping = true;
> -			vars->drop_next = cobalt_control(now,
> -							 p->interval,
> -							 vars->rec_inv_sqrt);
> +			WRITE_ONCE(vars->dropping, true);
> +			WRITE_ONCE(vars->drop_next,
> +				   cobalt_control(now,
> +						  p->interval,
> +						  vars->rec_inv_sqrt));
>  		}
> -		if (!vars->count)
> -			vars->count = 1;
> +		if (!count)
> +			count = 1;
>  	} else if (vars->dropping) {
> -		vars->dropping = false;
> +		WRITE_ONCE(vars->dropping, false);
>  	}
>  
>  	if (next_due && vars->dropping) {
>  		/* Use ECN mark if possible, otherwise drop */
> -		if (!(vars->ecn_marked = INET_ECN_set_ce(skb)))
> +		vars->ecn_marked = INET_ECN_set_ce(skb);
> +		if (!vars->ecn_marked)
>  			reason = QDISC_DROP_CONGESTED;
>  
> -		vars->count++;
> -		if (!vars->count)
> -			vars->count--;
> -		cobalt_invsqrt(vars);
> -		vars->drop_next = cobalt_control(vars->drop_next,
> -						 p->interval,
> -						 vars->rec_inv_sqrt);
> +		count++;
> +		if (!count)
> +			count--;
> +		cobalt_invsqrt(vars, count);
> +		WRITE_ONCE(vars->drop_next,
> +			   cobalt_control(vars->drop_next,
> +					  p->interval,
> +					  vars->rec_inv_sqrt));
>  		schedule = ktime_sub(now, vars->drop_next);
>  	} else {
>  		while (next_due) {
> -			vars->count--;
> -			cobalt_invsqrt(vars);
> -			vars->drop_next = cobalt_control(vars->drop_next,
> -							 p->interval,
> -							 vars->rec_inv_sqrt);
> +			count--;
> +			cobalt_invsqrt(vars, count);
> +			WRITE_ONCE(vars->drop_next,
> +				   cobalt_control(vars->drop_next,
> +						  p->interval,
> +						  vars->rec_inv_sqrt));
>  			schedule = ktime_sub(now, vars->drop_next);
> -			next_due = vars->count && ktime_to_ns(schedule) >= 0;
> +			next_due = count && ktime_to_ns(schedule) >= 0;
>  		}
>  	}
>  
> @@ -575,11 +588,12 @@ static enum qdisc_drop_reason cobalt_should_drop(struct cobalt_vars *vars,
>  	    get_random_u32() < vars->p_drop)
>  		reason = QDISC_DROP_FLOOD_PROTECTION;
>  
> +	WRITE_ONCE(vars->count, count);
>  	/* Overload the drop_next field as an activity timeout */
> -	if (!vars->count)
> -		vars->drop_next = ktime_add_ns(now, p->interval);
> +	if (count)

This seems to reverse the conditional?

> +		WRITE_ONCE(vars->drop_next, ktime_add_ns(now, p->interval));
>  	else if (ktime_to_ns(schedule) > 0 && reason == QDISC_DROP_UNSPEC)
> -		vars->drop_next = now;
> +		WRITE_ONCE(vars->drop_next, now);
>  
>  	return reason;
>  }
> @@ -813,7 +827,7 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
>  		     i++, k = (k + 1) % CAKE_SET_WAYS) {
>  			if (q->tags[outer_hash + k] == flow_hash) {
>  				if (i)
> -					q->way_hits++;
> +					WRITE_ONCE(q->way_hits, q->way_hits + 1);
>  
>  				if (!q->flows[outer_hash + k].set) {
>  					/* need to increment host refcnts */
> @@ -831,7 +845,7 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
>  		for (i = 0; i < CAKE_SET_WAYS;
>  			 i++, k = (k + 1) % CAKE_SET_WAYS) {
>  			if (!q->flows[outer_hash + k].set) {
> -				q->way_misses++;
> +				WRITE_ONCE(q->way_misses, q->way_misses + 1);
>  				allocate_src = cake_dsrc(flow_mode);
>  				allocate_dst = cake_ddst(flow_mode);
>  				goto found;
> @@ -841,7 +855,7 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
>  		/* With no empty queues, default to the original
>  		 * queue, accept the collision, update the host tags.
>  		 */
> -		q->way_collisions++;
> +		WRITE_ONCE(q->way_collisions, q->way_collisions + 1);
>  		allocate_src = cake_dsrc(flow_mode);
>  		allocate_dst = cake_ddst(flow_mode);
>  
> @@ -875,7 +889,8 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
>  			q->flows[reduced_hash].srchost = srchost_idx;
>  
>  			if (q->flows[reduced_hash].set == CAKE_SET_BULK)
> -				cake_inc_srchost_bulk_flow_count(q, &q->flows[reduced_hash], flow_mode);
> +				cake_inc_srchost_bulk_flow_count(q, &q->flows[reduced_hash],
> +								 flow_mode);
>  		}
>  
>  		if (allocate_dst) {
> @@ -899,7 +914,8 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
>  			q->flows[reduced_hash].dsthost = dsthost_idx;
>  
>  			if (q->flows[reduced_hash].set == CAKE_SET_BULK)
> -				cake_inc_dsthost_bulk_flow_count(q, &q->flows[reduced_hash], flow_mode);
> +				cake_inc_dsthost_bulk_flow_count(q, &q->flows[reduced_hash],
> +								 flow_mode);
>  		}
>  	}
>  
> @@ -1379,9 +1395,9 @@ static u32 cake_calc_overhead(struct cake_sched_data *qd, u32 len, u32 off)
>  		len -= off;
>  
>  	if (qd->max_netlen < len)
> -		qd->max_netlen = len;
> +		WRITE_ONCE(qd->max_netlen, len);
>  	if (qd->min_netlen > len)
> -		qd->min_netlen = len;
> +		WRITE_ONCE(qd->min_netlen, len);
>  
>  	len += q->rate_overhead;
>  
> @@ -1401,9 +1417,9 @@ static u32 cake_calc_overhead(struct cake_sched_data *qd, u32 len, u32 off)
>  	}
>  
>  	if (qd->max_adjlen < len)
> -		qd->max_adjlen = len;
> +		WRITE_ONCE(qd->max_adjlen, len);
>  	if (qd->min_adjlen > len)
> -		qd->min_adjlen = len;
> +		WRITE_ONCE(qd->min_adjlen, len);
>  
>  	return len;
>  }
> @@ -1416,7 +1432,7 @@ static u32 cake_overhead(struct cake_sched_data *q, const struct sk_buff *skb)
>  	u16 segs = qdisc_pkt_segs(skb);
>  	u32 len = qdisc_pkt_len(skb);
>  
> -	q->avg_netoff = cake_ewma(q->avg_netoff, off << 16, 8);
> +	WRITE_ONCE(q->avg_netoff, cake_ewma(q->avg_netoff, off << 16, 8));
>  
>  	if (segs == 1)
>  		return cake_calc_overhead(q, len, off);
> @@ -1590,16 +1606,17 @@ static unsigned int cake_drop(struct Qdisc *sch, struct sk_buff **to_free)
>  	}
>  
>  	if (cobalt_queue_full(&flow->cvars, &b->cparams, now))
> -		b->unresponsive_flow_count++;
> +		WRITE_ONCE(b->unresponsive_flow_count,
> +			   b->unresponsive_flow_count + 1);
>  
>  	len = qdisc_pkt_len(skb);
>  	q->buffer_used      -= skb->truesize;
> -	b->backlogs[idx]    -= len;
> -	b->tin_backlog      -= len;
> +	WRITE_ONCE(b->backlogs[idx], b->backlogs[idx] - len);
> +	WRITE_ONCE(b->tin_backlog, b->tin_backlog - len);
>  	qstats_backlog_sub(sch, len);
>  
> -	flow->dropped++;
> -	b->tin_dropped++;
> +	WRITE_ONCE(flow->dropped, flow->dropped + 1);
> +	WRITE_ONCE(b->tin_dropped, b->tin_dropped + 1);
>  
>  	if (q->config->rate_flags & CAKE_FLAG_INGRESS)
>  		cake_advance_shaper(q, b, skb, now, true);
> @@ -1795,7 +1812,7 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  	}
>  
>  	if (unlikely(len > b->max_skblen))
> -		b->max_skblen = len;
> +		WRITE_ONCE(b->max_skblen, len);
>  
>  	if (qdisc_pkt_segs(skb) > 1 && q->config->rate_flags & CAKE_FLAG_SPLIT_GSO) {
>  		struct sk_buff *segs, *nskb;
> @@ -1819,13 +1836,13 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  			numsegs++;
>  			slen += segs->len;
>  			q->buffer_used += segs->truesize;
> -			b->packets++;

Right above this hunk we do sch->q.qlen++; - does that need changing as
well?

>  		}
>  
>  		/* stats */
> -		b->bytes	    += slen;
> -		b->backlogs[idx]    += slen;
> -		b->tin_backlog      += slen;
> +		WRITE_ONCE(b->bytes, b->bytes + slen);
> +		WRITE_ONCE(b->packets, b->packets + numsegs);
> +		WRITE_ONCE(b->backlogs[idx], b->backlogs[idx] + slen);
> +		WRITE_ONCE(b->tin_backlog, b->tin_backlog + slen);
>  		qstats_backlog_add(sch, slen);
>  		q->avg_window_bytes += slen;
>  
> @@ -1843,10 +1860,10 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  			ack = cake_ack_filter(q, flow);
>  
>  		if (ack) {
> -			b->ack_drops++;
> +			WRITE_ONCE(b->ack_drops, b->ack_drops + 1);
>  			qdisc_qstats_drop(sch);
>  			ack_pkt_len = qdisc_pkt_len(ack);
> -			b->bytes += ack_pkt_len;
> +			WRITE_ONCE(b->bytes, b->bytes + ack_pkt_len);
>  			q->buffer_used += skb->truesize - ack->truesize;
>  			if (q->config->rate_flags & CAKE_FLAG_INGRESS)
>  				cake_advance_shaper(q, b, ack, now, true);
> @@ -1859,10 +1876,10 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  		}
>  
>  		/* stats */
> -		b->packets++;
> -		b->bytes	    += len - ack_pkt_len;
> -		b->backlogs[idx]    += len - ack_pkt_len;
> -		b->tin_backlog      += len - ack_pkt_len;
> +		WRITE_ONCE(b->packets, b->packets + 1);
> +		WRITE_ONCE(b->bytes, b->bytes + len - ack_pkt_len);
> +		WRITE_ONCE(b->backlogs[idx], b->backlogs[idx] + len - ack_pkt_len);
> +		WRITE_ONCE(b->tin_backlog, b->tin_backlog + len - ack_pkt_len);
>  		qstats_backlog_add(sch, len - ack_pkt_len);
>  		q->avg_window_bytes += len - ack_pkt_len;
>  	}
> @@ -1894,9 +1911,9 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  			u64 b = q->avg_window_bytes * (u64)NSEC_PER_SEC;
>  
>  			b = div64_u64(b, window_interval);
> -			q->avg_peak_bandwidth =
> -				cake_ewma(q->avg_peak_bandwidth, b,
> -					  b > q->avg_peak_bandwidth ? 2 : 8);
> +			WRITE_ONCE(q->avg_peak_bandwidth,
> +				   cake_ewma(q->avg_peak_bandwidth, b,
> +					     b > q->avg_peak_bandwidth ? 2 : 8));
>  			q->avg_window_bytes = 0;
>  			q->avg_window_begin = now;
>  
> @@ -1917,27 +1934,30 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
>  		if (!flow->set) {
>  			list_add_tail(&flow->flowchain, &b->new_flows);
>  		} else {
> -			b->decaying_flow_count--;
> +			WRITE_ONCE(b->decaying_flow_count,
> +				   b->decaying_flow_count - 1);
>  			list_move_tail(&flow->flowchain, &b->new_flows);
>  		}
>  		flow->set = CAKE_SET_SPARSE;
> -		b->sparse_flow_count++;
> +		WRITE_ONCE(b->sparse_flow_count,
> +			   b->sparse_flow_count + 1);
>  
> -		flow->deficit = cake_get_flow_quantum(b, flow, q->config->flow_mode);
> +		WRITE_ONCE(flow->deficit,
> +			   cake_get_flow_quantum(b, flow, q->config->flow_mode));
>  	} else if (flow->set == CAKE_SET_SPARSE_WAIT) {
>  		/* this flow was empty, accounted as a sparse flow, but actually
>  		 * in the bulk rotation.
>  		 */
>  		flow->set = CAKE_SET_BULK;
> -		b->sparse_flow_count--;
> -		b->bulk_flow_count++;
> +		WRITE_ONCE(b->sparse_flow_count, b->sparse_flow_count - 1);
> +		WRITE_ONCE(b->bulk_flow_count, b->bulk_flow_count + 1);
>  
>  		cake_inc_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
>  		cake_inc_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
>  	}
>  
>  	if (q->buffer_used > q->buffer_max_used)
> -		q->buffer_max_used = q->buffer_used;
> +		WRITE_ONCE(q->buffer_max_used, q->buffer_used);
>  
>  	if (q->buffer_used <= q->buffer_limit)
>  		return NET_XMIT_SUCCESS;
> @@ -1976,8 +1996,8 @@ static struct sk_buff *cake_dequeue_one(struct Qdisc *sch)
>  	if (flow->head) {
>  		skb = dequeue_head(flow);
>  		len = qdisc_pkt_len(skb);
> -		b->backlogs[q->cur_flow] -= len;
> -		b->tin_backlog		 -= len;
> +		WRITE_ONCE(b->backlogs[q->cur_flow], b->backlogs[q->cur_flow] - len);
> +		WRITE_ONCE(b->tin_backlog, b->tin_backlog - len);
>  		qstats_backlog_sub(sch, len);
>  		q->buffer_used		 -= skb->truesize;
>  		qdisc_qlen_dec(sch);
> @@ -2042,7 +2062,7 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  
>  		cake_configure_rates(sch, new_rate, true);
>  		q->last_checked_active = now;
> -		q->active_queues = num_active_qs;
> +		WRITE_ONCE(q->active_queues, num_active_qs);
>  	}
>  
>  begin:
> @@ -2149,8 +2169,10 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  		 */
>  		if (flow->set == CAKE_SET_SPARSE) {
>  			if (flow->head) {
> -				b->sparse_flow_count--;
> -				b->bulk_flow_count++;
> +				WRITE_ONCE(b->sparse_flow_count,
> +					   b->sparse_flow_count - 1);
> +				WRITE_ONCE(b->bulk_flow_count,
> +					   b->bulk_flow_count + 1);
>  
>  				cake_inc_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
>  				cake_inc_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
> @@ -2165,7 +2187,8 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  			}
>  		}
>  
> -		flow->deficit += cake_get_flow_quantum(b, flow, q->config->flow_mode);
> +		WRITE_ONCE(flow->deficit,
> +			   flow->deficit + cake_get_flow_quantum(b, flow, q->config->flow_mode));
>  		list_move_tail(&flow->flowchain, &b->old_flows);
>  
>  		goto retry;
> @@ -2177,7 +2200,8 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  		if (!skb) {
>  			/* this queue was actually empty */
>  			if (cobalt_queue_empty(&flow->cvars, &b->cparams, now))
> -				b->unresponsive_flow_count--;
> +				WRITE_ONCE(b->unresponsive_flow_count,
> +					   b->unresponsive_flow_count - 1);
>  
>  			if (flow->cvars.p_drop || flow->cvars.count ||
>  			    ktime_before(now, flow->cvars.drop_next)) {
> @@ -2187,16 +2211,22 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  				list_move_tail(&flow->flowchain,
>  					       &b->decaying_flows);
>  				if (flow->set == CAKE_SET_BULK) {
> -					b->bulk_flow_count--;
> +					WRITE_ONCE(b->bulk_flow_count,
> +						   b->bulk_flow_count - 1);
>  
> -					cake_dec_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
> -					cake_dec_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
> +					cake_dec_srchost_bulk_flow_count(b, flow,
> +									 q->config->flow_mode);
> +					cake_dec_dsthost_bulk_flow_count(b, flow,
> +									 q->config->flow_mode);

These seem like unnecessary whitespace changes?

>  
> -					b->decaying_flow_count++;
> +					WRITE_ONCE(b->decaying_flow_count,
> +						   b->decaying_flow_count + 1);
>  				} else if (flow->set == CAKE_SET_SPARSE ||
>  					   flow->set == CAKE_SET_SPARSE_WAIT) {
> -					b->sparse_flow_count--;
> -					b->decaying_flow_count++;
> +					WRITE_ONCE(b->sparse_flow_count,
> +						   b->sparse_flow_count - 1);
> +					WRITE_ONCE(b->decaying_flow_count,
> +						   b->decaying_flow_count + 1);
>  				}
>  				flow->set = CAKE_SET_DECAYING;
>  			} else {
> @@ -2204,14 +2234,20 @@ static struct sk_buff *cake_dequeue(struct Qdisc *sch)
>  				list_del_init(&flow->flowchain);
>  				if (flow->set == CAKE_SET_SPARSE ||
>  				    flow->set == CAKE_SET_SPARSE_WAIT)
> -					b->sparse_flow_count--;
> +					WRITE_ONCE(b->sparse_flow_count,
> +						   b->sparse_flow_count - 1);
>  				else if (flow->set == CAKE_SET_BULK) {
> -					b->bulk_flow_count--;
> +					WRITE_ONCE(b->bulk_flow_count,
> +						   b->bulk_flow_count - 1);
>  
> -					cake_dec_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
> -					cake_dec_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);

Same here?

-Toke


^ permalink raw reply

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

Thu, Apr 09, 2026 at 04:50:52AM +0200, rkannoth@marvell.com wrote:
>From: Saeed Mahameed <saeedm@nvidia.com>

[...]


>diff --git a/net/devlink/param.c b/net/devlink/param.c
>index 4595fffbd825..8c9165797b32 100644
>--- a/net/devlink/param.c
>+++ b/net/devlink/param.c
>@@ -252,6 +252,14 @@ devlink_nl_param_value_put(struct sk_buff *msg, enum devlink_param_type type,
> 				return -EMSGSIZE;
> 		}
> 		break;
>+	case DEVLINK_PARAM_TYPE_U64_ARRAY:
>+		if (val->u64arr.size > __DEVLINK_PARAM_MAX_ARRAY_SIZE)

From UAPI perspective, what's the motivation for such limitation? I
don't think we need it. Whatever kernel/user fits into skb is okay, no?




>+			return -EMSGSIZE;
>+
>+		for (int i = 0; i < val->u64arr.size; i++)
>+			if (nla_put_uint(msg, nla_type, val->u64arr.val[i]))
>+				return -EMSGSIZE;
>+		break;
> 	}
> 	return 0;
> }

[...]

^ permalink raw reply

* Re: [PATCH net] net: usb: cdc_ncm: reject negative chained NDP offsets
From: Oliver Neukum @ 2026-04-13 12:11 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Oliver Neukum
  Cc: linux-usb, netdev, linux-kernel, Oliver Neukum, Andrew Lunn,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	stable
In-Reply-To: <2026041325-giggly-wrecking-e6ef@gregkh>

On 13.04.26 12:43, Greg Kroah-Hartman wrote:
> On Mon, Apr 13, 2026 at 10:36:19AM +0200, Oliver Neukum wrote:
>>
>>
>> On 11.04.26 12:53, Greg Kroah-Hartman wrote:
>>> cdc_ncm_rx_fixup() reads dwNextNdpIndex from each NDP32 to chain to the
>>> next one.  The 32-bit value from the device is stored into the signed
>>> int ndpoffset so that means values with the high bit set become
>>
>> Well, then isn't the problem rather that you should not store an
>> unsigned value in a signed variable?
> 
> No.  well, yes.  but no.
> 
> cdc_ncm_rx_verify_nth16() returns an int, and is negative if something
> went wrong, so we need it that way, and then we need to check it, like
> we properly do at the top of the loop, it's just that at the bottom of
> the loop we also need to do the same exact thing.

Doesn't that suggest that cdc_ncm_rx_verify_nth16() is the problem?
To be precise, the way it indicates errors?
As this is an offset into a buffer and the header must be at the start
of the buffer, isn't 0 the natural indication of an error?

	Regards
		Oliver
  

^ permalink raw reply

* [PATCH net v2 1/1] af_unix: Reject SIOCATMARK on non-stream sockets
From: Ren Wei @ 2026-04-13 12:29 UTC (permalink / raw)
  To: netdev
  Cc: kuniyu, davem, edumazet, kuba, pabeni, horms, rao.shoaib,
	yifanwucs, tomapufckgml, yuantan098, bird, enjou1224z,
	wangjiexun2025, n05ec

From: Jiexun Wang <wangjiexun2025@gmail.com>

SIOCATMARK reports whether the receive queue is at the urgent mark for
MSG_OOB.

In AF_UNIX, MSG_OOB is supported only for SOCK_STREAM sockets.
SOCK_DGRAM and SOCK_SEQPACKET reject MSG_OOB in sendmsg() and recvmsg(),
so they should not support SIOCATMARK either.

Return -EOPNOTSUPP for non-stream sockets before checking the receive
queue.

Fixes: 314001f0bf92 ("af_unix: Add OOB support")
Reported-by: Yifan Wu <yifanwucs@gmail.com>
Reported-by: Juefei Pu <tomapufckgml@gmail.com>
Co-developed-by: Yuan Tan <yuantan098@gmail.com>
Signed-off-by: Yuan Tan <yuantan098@gmail.com>
Suggested-by: Xin Liu <bird@lzu.edu.cn>
Tested-by: Ren Wei <enjou1224z@gmail.com>
Signed-off-by: Jiexun Wang <wangjiexun2025@gmail.com>
Signed-off-by: Ren Wei <n05ec@lzu.edu.cn>
---
Changes in v2:
- Rework the fix based on maintainer feedback.
- Drop the receive-queue locking approach and reject SIOCATMARK on
  non-stream sockets instead, since it is only meaningful for MSG_OOB.
- V1 link: https://lore.kernel.org/netdev/f6cbbc8da90e95584847b5ceb60aae830d1631c2.1775731983.git.wangjiexun2025@gmail.com/

 net/unix/af_unix.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c
index b23c33df8b46..09d43b4813b1 100644
--- a/net/unix/af_unix.c
+++ b/net/unix/af_unix.c
@@ -3300,6 +3300,9 @@ static int unix_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
 			struct sk_buff *skb;
 			int answ = 0;
 
+			if (sk->sk_type != SOCK_STREAM)
+				return -EOPNOTSUPP;
+
 			mutex_lock(&u->iolock);
 
 			skb = skb_peek(&sk->sk_receive_queue);
-- 
2.34.1


^ permalink raw reply related

* Re: [PATCH net 1/2] netfilter: skip recording stale or retransmitted INIT
From: Marcelo Ricardo Leitner @ 2026-04-13 12:35 UTC (permalink / raw)
  To: Xin Long
  Cc: network dev, linux-sctp, davem, kuba, Eric Dumazet, Paolo Abeni,
	Simon Horman, Florian Westphal, Yi Chen
In-Reply-To: <6e09f9a8d1f13f3ce691c696d3dd7b2a2e6c6184.1775847557.git.lucien.xin@gmail.com>

On Fri, Apr 10, 2026 at 02:59:16PM -0400, Xin Long wrote:
> An INIT whose init_tag matches the peer's vtag does not provide new state
> information. It indicates either:
> 
> - a stale INIT (after INIT-ACK has already been seen on the same side), or
> - a retransmitted INIT (after INIT has already been recorded on the same
>   side).
> 
> In both cases, the INIT must not update ct->proto.sctp.init[] state, since
> it does not advance the handshake tracking and may otherwise corrupt
> INIT/INIT-ACK validation logic.
> 
> Allow INIT processing only when the conntrack entry is newly created
> (SCTP_CONNTRACK_NONE), or when the init_tag differs from the stored peer
> vtag.
> 
> Note it skips the check for the ct with old_state SCTP_CONNTRACK_NONE in
> nf_conntrack_sctp_packet(), as it is just created in sctp_new() where it
> set ct->proto.sctp.vtag[IP_CT_DIR_REPLY] = ih->init_tag.
> 
> Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
> Signed-off-by: Xin Long <lucien.xin@gmail.com>

Reviewed-by: Marcelo Ricardo Leitner <marcelo.leitner@gmail.com>

^ permalink raw reply

* Re: [PATCH net 2/2] sctp: discard stale INIT after handshake completion
From: Marcelo Ricardo Leitner @ 2026-04-13 12:36 UTC (permalink / raw)
  To: Xin Long
  Cc: network dev, linux-sctp, davem, kuba, Eric Dumazet, Paolo Abeni,
	Simon Horman, Florian Westphal, Yi Chen
In-Reply-To: <bea8a0dfcc56b9980cb914b54cffa9dd9948ba75.1775847557.git.lucien.xin@gmail.com>

On Fri, Apr 10, 2026 at 02:59:17PM -0400, Xin Long wrote:
> After an association reaches ESTABLISHED, the peer’s init_tag is already
> known from the handshake. Any subsequent INIT with the same init_tag is
> not a valid restart, but a delayed or duplicate INIT.
> 
> Drop such INIT chunks in sctp_sf_do_unexpected_init() instead of
> processing them as new association attempts.
> 
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Signed-off-by: Xin Long <lucien.xin@gmail.com>

Acked-by: Marcelo Ricardo Leitner <marcelo.leitner@gmail.com>

^ permalink raw reply

* Re: [PATCH net] sctp: fix missing encap_port propagation for GSO fragments
From: Marcelo Ricardo Leitner @ 2026-04-13 12:37 UTC (permalink / raw)
  To: Xin Long
  Cc: network dev, linux-sctp, davem, kuba, Eric Dumazet, Paolo Abeni,
	Simon Horman
In-Reply-To: <ea65ed61b3598d8b4940f0170b9aa1762307e6c3.1776017631.git.lucien.xin@gmail.com>

On Sun, Apr 12, 2026 at 02:13:51PM -0400, Xin Long wrote:
> encap_port in SCTP_INPUT_CB(skb) is used by sctp_vtag_verify() for
> SCTP-over-UDP processing. In the GSO case, it is only set on the head
> skb, while fragment skbs leave it 0.
> 
> This results in fragment skbs seeing encap_port == 0, breaking
> SCTP-over-UDP connections.
> 
> Fix it by propagating encap_port from the head skb cb when initializing
> fragment skbs in sctp_inq_pop().
> 
> Fixes: 046c052b475e ("sctp: enable udp tunneling socks")
> Signed-off-by: Xin Long <lucien.xin@gmail.com>

Acked-by: Marcelo Ricardo Leitner <marcelo.leitner@gmail.com>

^ permalink raw reply

* Re: [PATCH net] sctp: disable BH before calling udp_tunnel_xmit_skb()
From: Marcelo Ricardo Leitner @ 2026-04-13 12:39 UTC (permalink / raw)
  To: Xin Long
  Cc: network dev, linux-sctp, davem, kuba, Eric Dumazet, Paolo Abeni,
	Simon Horman, Weiming Shi
In-Reply-To: <c874a8548221dcd56ff03c65ba75a74e6cf99119.1776017727.git.lucien.xin@gmail.com>

On Sun, Apr 12, 2026 at 02:15:27PM -0400, Xin Long wrote:
> udp_tunnel_xmit_skb() / udp_tunnel6_xmit_skb() are expected to run with
> BH disabled.  After commit 6f1a9140ecda ("add xmit recursion limit to
> tunnel xmit functions"), on the path:
> 
>   udp(6)_tunnel_xmit_skb() -> ip(6)tunnel_xmit()
> 
> dev_xmit_recursion_inc()/dec() must stay balanced on the same CPU.
> 
> Without local_bh_disable(), the context may move between CPUs, which can
> break the inc/dec pairing. This may lead to incorrect recursion level
> detection and cause packets to be dropped in ip(6)_tunnel_xmit() or
> __dev_queue_xmit().
> 
> Fix it by disabling BH around both IPv4 and IPv6 SCTP UDP xmit paths.
> 
> In my testing, after enabling the SCTP over UDP:
> 
>   # ip net exec ha sysctl -w net.sctp.udp_port=9899
>   # ip net exec ha sysctl -w net.sctp.encap_port=9899
>   # ip net exec hb sysctl -w net.sctp.udp_port=9899
>   # ip net exec hb sysctl -w net.sctp.encap_port=9899
> 
>   # ip net exec ha iperf3 -s
> 
> - without this patch:
> 
>   # ip net exec hb iperf3 -c 192.168.0.1 --sctp
>   [  5]   0.00-10.00  sec  37.2 MBytes  31.2 Mbits/sec  sender
>   [  5]   0.00-10.00  sec  37.1 MBytes  31.1 Mbits/sec  receiver
> 
> - with this patch:
> 
>   # ip net exec hb iperf3 -c 192.168.0.1 --sctp
>   [  5]   0.00-10.00  sec  3.14 GBytes  2.69 Gbits/sec  sender
>   [  5]   0.00-10.00  sec  3.14 GBytes  2.69 Gbits/sec  receiver
> 
> Fixes: 6f1a9140ecda ("add xmit recursion limit to tunnel xmit functions")
> Fixes: 046c052b475e ("sctp: enable udp tunneling socks")
> Signed-off-by: Xin Long <lucien.xin@gmail.com>

Nice catch!

Acked-by: Marcelo Ricardo Leitner <marcelo.leitner@gmail.com>

^ permalink raw reply

* Re: [PATCH net] net: usb: cdc_ncm: reject negative chained NDP offsets
From: Greg Kroah-Hartman @ 2026-04-13 12:24 UTC (permalink / raw)
  To: Oliver Neukum
  Cc: linux-usb, netdev, linux-kernel, Oliver Neukum, Andrew Lunn,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	stable
In-Reply-To: <198c1240-80a6-456c-8b12-25158c90c965@suse.com>

On Mon, Apr 13, 2026 at 02:11:50PM +0200, Oliver Neukum wrote:
> On 13.04.26 12:43, Greg Kroah-Hartman wrote:
> > On Mon, Apr 13, 2026 at 10:36:19AM +0200, Oliver Neukum wrote:
> > > 
> > > 
> > > On 11.04.26 12:53, Greg Kroah-Hartman wrote:
> > > > cdc_ncm_rx_fixup() reads dwNextNdpIndex from each NDP32 to chain to the
> > > > next one.  The 32-bit value from the device is stored into the signed
> > > > int ndpoffset so that means values with the high bit set become
> > > 
> > > Well, then isn't the problem rather that you should not store an
> > > unsigned value in a signed variable?
> > 
> > No.  well, yes.  but no.
> > 
> > cdc_ncm_rx_verify_nth16() returns an int, and is negative if something
> > went wrong, so we need it that way, and then we need to check it, like
> > we properly do at the top of the loop, it's just that at the bottom of
> > the loop we also need to do the same exact thing.
> 
> Doesn't that suggest that cdc_ncm_rx_verify_nth16() is the problem?
> To be precise, the way it indicates errors?
> As this is an offset into a buffer and the header must be at the start
> of the buffer, isn't 0 the natural indication of an error?

Maybe?  I really don't know, sorry, parsing the cdc_ncm buffer is not
something I looked too deeply into :)

greg k-h

^ 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