From: Jamal Hadi Salim <jhs@mojatatu.com>
To: netdev@vger.kernel.org
Cc: Jamal Hadi Salim <jhs@mojatatu.com>,
shuah@kernel.org, linux-kselftest@vger.kernel.org,
davem@davemloft.net, edumazet@kernel.org, kuba@kernel.org,
pabeni@redhat.com, horms@kernel.org, jiri@resnulli.us,
bpf@vger.kernel.org, victor@mojatatu.com
Subject: [PATCH net v2 3/3] selftests: net: exercise dev-bound cls_bpf on shared blocks
Date: Sat, 10 Oct 2026 17:02:50 -0400 [thread overview]
Message-ID: <QDISC-PYC7.v2.20261010165947-4@mojatatu.com> (raw)
In-Reply-To: <QDISC-PYC7.v2.20261010165947@mojatatu.com>
The issue:
bpf_offload.py only exercises per-device ingress qdiscs today, so neither
the shared-block accept path nor the late-join replay is tested.
The Fix:
Add a test for a device-bound SCHED_CLS program attached through a shared
block. It loads a dev-bound SCHED_CLS program for a netdevsim device and
checks:
- the program attaches to a shared block owned by its device, and the
filter teardown leaves the device unloaded;
- a block that also holds an independently backed device rejects the
attach, because the offload path broadcasts the program to all
members;
- an independently backed device that joins a populated block is not
loaded by the bind/replay path, and the driver's extack is asserted;
- a second port of the same netdevsim device (same offload device) is
accepted together with the bound port.
The program is loaded from the existing sample_ret0.bpf.o with an explicit
`type classifier` override, so no new sample object is needed.
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v1 -> v2 (v2 of an on-list patch; v1 posted 2026-10-04):
- New selftest: shared-block + late-join coverage in bpf_offload.py.
- Split the single v2 draft into three patches; this companion carries no
Fixes: and no Cc: stable (a selftest is not backported; it depends on
both code patches).
---
tools/testing/selftests/net/bpf_offload.py | 139 +++++++++++++++++++++
1 file changed, 139 insertions(+)
diff --git a/tools/testing/selftests/net/bpf_offload.py b/tools/testing/selftests/net/bpf_offload.py
index c856d266c8f3..87e244d8ff05 100755
--- a/tools/testing/selftests/net/bpf_offload.py
+++ b/tools/testing/selftests/net/bpf_offload.py
@@ -452,6 +452,38 @@ class BpfNetdevSim(NetdevSim):
self.tc_add_ingress()
self.wait_for_flush(bound=bound, total=total)
+ def tc_add_block(self, block, fail=True, include_stderr=False):
+ return tc("qdisc add dev %s ingress_block %d clsact" %
+ (self['ifname'], block), fail=fail,
+ include_stderr=include_stderr)
+
+ def tc_del_block(self, block):
+ tc("qdisc del dev %s clsact" % (self['ifname']), fail=False)
+
+ def tc_add_block_filter(self, block, bpf, op="add", prio=None, handle=None,
+ da=False, verbose=False, skip_sw=False,
+ skip_hw=False, fail=True, include_stderr=False):
+ spec = ""
+ if prio is not None:
+ spec += " prio %d" % (prio)
+ if handle:
+ spec += " handle %s" % (handle)
+
+ params = ""
+ if da:
+ params += " da"
+ if verbose:
+ params += " verbose"
+ if skip_sw:
+ params += " skip_sw"
+ if skip_hw:
+ params += " skip_hw"
+
+ return tc("filter {op} block {block} {spec} bpf {bpf} {params}"
+ .format(op=op, block=block, spec=spec, bpf=bpf,
+ params=params),
+ fail=fail, include_stderr=include_stderr)
+
def tc_show_ingress(self, expected=None):
# No JSON support, oh well...
flags = ["skip_sw", "skip_hw", "in_hw"]
@@ -629,6 +661,111 @@ def test_spurios_extack(sim, obj, skip_hw, needle):
include_stderr=True)
check_no_extack(res, needle)
+def test_tc_shared_block():
+ """
+ Device-bound cls_bpf program attached through a shared block.
+ """
+ # A shared block has no qdisc, so the target device is derived from the
+ # block's bound netdevs and the program must match every member, since
+ # the offload path broadcasts it to every callback on the block. A
+ # netdev joining a populated block later is replayed the stored filter
+ # before it is inserted into block->ports, so the driver callback must
+ # enforce the device match as well.
+ simdev = BpfNetdevSimDev()
+ sim, = simdev.nsims
+ sim.set_ethtool_tc_offloads(True)
+
+ start_test("Test TC shared-block load dev-bound program...")
+ bpftool_prog_load("sample_ret0.bpf.o", "/sys/fs/bpf/tc_shared",
+ prog_type="classifier", dev=sim['ifname'])
+
+ start_test("Test TC shared-block dev-bound attach...")
+ # The pinned program is dev-bound to this device; attaching it through
+ # a shared block must succeed even though block->q is NULL.
+ sim.tc_add_block(22)
+ # err is kept (include_stderr) for post-mortem value on a failure.
+ ret, _, err = sim.tc_add_block_filter(22, bpf_pinned("/sys/fs/bpf/tc_shared"),
+ da=True, skip_sw=True,
+ fail=False, include_stderr=True)
+ fail(ret != 0, "dev-bound program rejected on a matching shared block")
+ fail(sim.dfs_read("bpf_offloaded_id") == "0",
+ "dev-bound program not offloaded on a matching shared block")
+ sim.tc_del_block(22)
+
+ start_test("Test TC shared-block broadcast to a foreign member...")
+ # Add an independently backed device to the SAME block before the
+ # attach. The program is bound to sim's device only, and the offload
+ # path fans it out to every member, so the attach must be rejected.
+ simdev2 = BpfNetdevSimDev()
+ sim2, = simdev2.nsims
+ sim2.set_ethtool_tc_offloads(True)
+ sim.tc_add_block(40)
+ sim2.tc_add_block(40)
+ ret, _, err = sim.tc_add_block_filter(40, bpf_pinned("/sys/fs/bpf/tc_shared"),
+ da=True, skip_sw=True,
+ fail=False, include_stderr=True)
+ fail(ret == 0, "dev-bound program broadcast to a foreign shared-block member")
+ check_extack(err, "Program is not bound to every device in the block.", args)
+ sim2.tc_del_block(40)
+ sim.tc_del_block(40)
+ simdev2.remove()
+
+ start_test("Test TC shared-block late join replay...")
+ # Build a block owned by sim's device with the matching program
+ # installed, then let an independently backed device join it. The
+ # join replays the stored filter to the joining device's callback
+ # before the device is recorded in block->ports; the driver must
+ # reject the replay so the foreign device is never loaded.
+ # sim2 is created here (not inherited) so the TC-offload state it
+ # depends on is explicit.
+ simdev2 = BpfNetdevSimDev()
+ sim2, = simdev2.nsims
+ sim2.set_ethtool_tc_offloads(True)
+ sim.tc_add_block(30)
+ ret, _, err = sim.tc_add_block_filter(30, bpf_pinned("/sys/fs/bpf/tc_shared"),
+ da=True, skip_sw=True,
+ fail=False, include_stderr=True)
+ fail(ret != 0, "dev-bound program rejected on a matching shared block")
+ ret, _, err = sim2.tc_add_block(30, fail=False, include_stderr=True)
+ fail(ret == 0, "foreign device joined a populated dev-bound shared block")
+ check_extack_nsim(err, "program bound to a different device.", args)
+ fail(sim2.dfs_read("bpf_offloaded_id") != "0",
+ "replayed program loaded onto a foreign device")
+ # The add above was expected to fail, so the qdisc may not exist; the
+ # delete is harmless and only cleans up when it does.
+ sim2.tc_del_block(30)
+ sim.tc_del_block(30)
+ simdev2.remove()
+
+ start_test("Test TC shared-block same-offload-device member...")
+ # A second port of the same netdevsim device shares the offload device,
+ # so a program bound to one port may legitimately be offloaded to both.
+ simdev3 = BpfNetdevSimDev(2)
+ sim3, sim3b = simdev3.nsims
+ sim3.set_ethtool_tc_offloads(True)
+ sim3b.set_ethtool_tc_offloads(True)
+ bpftool_prog_load("sample_ret0.bpf.o", "/sys/fs/bpf/tc_shared_multi",
+ prog_type="classifier", dev=sim3['ifname'])
+ sim3.tc_add_block(50)
+ sim3b.tc_add_block(50)
+ # err kept for post-mortem value on a failure.
+ ret, _, err = sim3.tc_add_block_filter(50, bpf_pinned("/sys/fs/bpf/tc_shared_multi"),
+ da=True, skip_sw=True,
+ fail=False, include_stderr=True)
+ fail(ret != 0, "same-offload-device member rejected on a shared block")
+ fail(sim3.dfs_read("bpf_offloaded_id") == "0",
+ "program not offloaded to the bound port")
+ fail(sim3b.dfs_read("bpf_offloaded_id") == "0",
+ "program not offloaded to the same-offload-device port")
+ sim3b.tc_del_block(50)
+ sim3.tc_del_block(50)
+
+ simdev3.remove()
+ simdev.remove()
+ rm("/sys/fs/bpf/tc_shared_multi")
+ rm("/sys/fs/bpf/tc_shared")
+ bpftool_prog_list_wait(expected=0)
+
def test_multi_prog(simdev, sim, obj, modename, modeid):
start_test("Test multi-attachment XDP - %s + offload..." %
(modename or "default", ))
@@ -1349,6 +1486,8 @@ try:
ret, out = bpftool("prog show %s" % (progB), fail=False)
fail(ret != 0, "couldn't get information about orphaned program")
+ test_tc_shared_block()
+
print("%s: OK" % (os.path.basename(__file__)))
finally:
--
2.43.0
next prev parent reply other threads:[~2026-10-10 21:03 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-10 21:02 [PATCH net v2 0/3] net/sched: cls_bpf: accept dev-bound programs on shared blocks Jamal Hadi Salim
2026-10-10 21:02 ` [PATCH net v2 1/3] netdevsim: reject cls_bpf programs bound to another device Jamal Hadi Salim
2026-10-11 21:03 ` netdev-bot+sashiko
2026-10-11 23:50 ` Jamal Hadi Salim
2026-10-10 21:02 ` [PATCH net v2 2/3] net/sched: cls_bpf: accept dev-bound programs on shared blocks Jamal Hadi Salim
2026-10-11 21:03 ` netdev-bot+sashiko
2026-10-11 23:51 ` Jamal Hadi Salim
2026-10-10 21:02 ` Jamal Hadi Salim [this message]
2026-10-11 21:03 ` [PATCH net v2 3/3] selftests: net: exercise dev-bound cls_bpf " netdev-bot+sashiko
2026-10-11 23:49 ` Jamal Hadi Salim
2026-10-11 23:50 ` Jamal Hadi Salim
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=QDISC-PYC7.v2.20261010165947-4@mojatatu.com \
--to=jhs@mojatatu.com \
--cc=bpf@vger.kernel.org \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shuah@kernel.org \
--cc=victor@mojatatu.com \
/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