From: Reshma Sreekumar <reshmaisat@gmail.com>
To: Jamal Hadi Salim <jhs@mojatatu.com>,
Jiri Pirko <jiri@resnulli.us>,
netdev@vger.kernel.org
Cc: "David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>, Shuah Khan <shuah@kernel.org>,
linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org,
Reshma Sreekumar <reshmaisat@gmail.com>
Subject: [PATCH net 2/2] selftests: net: check that a disturbed qdisc dump is flagged
Date: Thu, 30 Jul 2026 12:00:06 +0200 [thread overview]
Message-ID: <20260730100006.3547944-3-reshmaisat@gmail.com> (raw)
In-Reply-To: <20260730100006.3547944-1-reshmaisat@gmail.com>
Add a test for NLM_F_DUMP_INTR on RTM_GETQDISC. A qdisc dump spans several
netlink batches once a few devices carry a full set of qdiscs, and RTNL is
only held while a single batch is filled, so the device list can change in
between and entries can be missed.
Netlink dumps are driven by the reader, so rather than racing against a
dump the test unregisters a device at a known point: it receives the first
batch, unregisters a dummy, then drains the rest. This is deterministic --
one flagged message, every run.
A second case dumps without disturbing anything and requires that the flag
stays clear, so the test cannot be satisfied by a kernel that raises it
unconditionally.
Four dummy devices with mq + 16 fq + clsact each, read with a 4KiB buffer,
give 12 batches and run in about a second. The test skips if dummy, mq, fq
or clsact are unavailable.
Signed-off-by: Reshma Sreekumar <reshmaisat@gmail.com>
---
tools/testing/selftests/net/Makefile | 1 +
.../testing/selftests/net/qdisc_dump_intr.py | 123 ++++++++++++++++++
2 files changed, 124 insertions(+)
create mode 100755 tools/testing/selftests/net/qdisc_dump_intr.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 708d960ae..769ee26ae 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -72,6 +72,7 @@ TEST_PROGS := \
pmtu.sh \
protodown.sh \
psock_snd.sh \
+ qdisc_dump_intr.py \
reuseaddr_ports_exhausted.sh \
reuseport_addr_any.sh \
route_hint.sh \
diff --git a/tools/testing/selftests/net/qdisc_dump_intr.py b/tools/testing/selftests/net/qdisc_dump_intr.py
new file mode 100755
index 000000000..9623847f4
--- /dev/null
+++ b/tools/testing/selftests/net/qdisc_dump_intr.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Check that a qdisc dump disturbed by a device unregister reports
+NLM_F_DUMP_INTR.
+
+tc_dump_qdisc() walks every netdev in the netns and spans several netlink
+batches once there are more than a handful of qdiscs. RTNL is only held
+while a single batch is filled, so the device list can change in between and
+the reply can miss entries. Netlink dumps are driven by the reader, so this
+test unregisters a device at a known point between two batches rather than
+racing against one.
+"""
+
+import socket
+import struct
+
+from lib.py import ksft_run, ksft_exit, ksft_true, ksft_pr, KsftSkipEx
+from lib.py import CmdExitFailure, NetNS, NetNSEnter, ip, tc
+
+NETLINK_ROUTE = 0
+RTM_NEWQDISC = 36
+RTM_GETQDISC = 38
+NLMSG_DONE = 3
+NLM_F_REQUEST = 0x001
+NLM_F_DUMP = 0x300
+NLM_F_DUMP_INTR = 0x010
+
+NLMSGHDR = "=IHHII"
+TCMSG_LEN = 20 # struct tcmsg
+
+# tc parses handles as hex; 64: keeps the mq root clear of the fq children
+MQ_HANDLE = "64"
+CHANNELS = 16 # fq children per device
+NDEVS = 4 # 4 * (mq + 16 fq + clsact) is several batches worth
+BUFSZ = 4096 # small recv buffer keeps the kernel's batches small
+
+
+def _setup():
+ """Devices carrying enough qdiscs that a dump has to paginate."""
+ try:
+ for i in range(NDEVS):
+ ip(f"link add d{i} numtxqueues {CHANNELS} type dummy")
+ ip(f"link set d{i} up")
+ tc(f"qdisc replace dev d{i} root handle {MQ_HANDLE}: mq")
+ for q in range(1, CHANNELS + 1):
+ tc(f"qdisc replace dev d{i} parent {MQ_HANDLE}:{q:x} "
+ f"handle {q:x}: fq")
+ tc(f"qdisc replace dev d{i} clsact")
+ except CmdExitFailure as exc:
+ raise KsftSkipEx("dummy, mq, fq or clsact unavailable") from exc
+
+
+def _dump(between_batches=None):
+ """
+ Walk an RTM_GETQDISC dump one batch at a time, optionally running
+ `between_batches` after the first batch. Returns (batches, qdiscs,
+ messages carrying NLM_F_DUMP_INTR).
+ """
+ sk = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_ROUTE)
+ try:
+ sk.bind((0, 0))
+ hdr = struct.pack(NLMSGHDR, struct.calcsize(NLMSGHDR) + TCMSG_LEN,
+ RTM_GETQDISC, NLM_F_REQUEST | NLM_F_DUMP, 1, 0)
+ sk.send(hdr + bytes(TCMSG_LEN))
+
+ batches = qdiscs = intr = 0
+ done = False
+ while not done:
+ buf = sk.recv(BUFSZ)
+ batches += 1
+ off = 0
+ while off + 16 <= len(buf):
+ mlen, mtype, flags = struct.unpack_from("=IHH", buf, off)
+ if mlen < 16:
+ break
+ if flags & NLM_F_DUMP_INTR:
+ intr += 1
+ if mtype == NLMSG_DONE:
+ done = True
+ elif mtype == RTM_NEWQDISC:
+ qdiscs += 1
+ off += (mlen + 3) & ~3
+ if batches == 1 and not done and between_batches:
+ between_batches()
+ return batches, qdiscs, intr
+ finally:
+ sk.close()
+
+
+def dump_intr_on_unregister() -> None:
+ """Unregistering a device mid-dump must raise NLM_F_DUMP_INTR."""
+ ip("link add victim type dummy")
+
+ batches, qdiscs, intr = _dump(lambda: ip("link del victim"))
+
+ ksft_pr(f"{batches} batches, {qdiscs} qdiscs, {intr} flagged messages")
+ ksft_true(batches > 1, "dump did not paginate, nothing to disturb")
+ ksft_true(intr > 0,
+ "NLM_F_DUMP_INTR not set although a device was unregistered "
+ "while the dump was in flight")
+
+
+def dump_intr_absent_when_stable() -> None:
+ """An undisturbed dump must not raise NLM_F_DUMP_INTR."""
+ batches, qdiscs, intr = _dump()
+
+ ksft_pr(f"{batches} batches, {qdiscs} qdiscs, {intr} flagged messages")
+ ksft_true(batches > 1, "dump did not paginate, test is not meaningful")
+ ksft_true(intr == 0, "NLM_F_DUMP_INTR set on an undisturbed dump")
+
+
+def main() -> None:
+ with NetNS() as ns:
+ with NetNSEnter(str(ns)):
+ _setup()
+ ksft_run(globs=globals(), case_pfx={"dump_"})
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()
--
2.43.0
prev parent reply other threads:[~2026-07-30 10:00 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-30 10:00 [PATCH net 0/2] net/sched: flag inconsistent qdisc dumps Reshma Sreekumar
2026-07-30 10:00 ` [PATCH net 1/2] " Reshma Sreekumar
2026-07-30 10:25 ` Eric Dumazet
2026-07-30 10:00 ` Reshma Sreekumar [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260730100006.3547944-3-reshmaisat@gmail.com \
--to=reshmaisat@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox