BPF List
 help / color / mirror / Atom feed
* [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net
@ 2026-10-16 17:43 Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 01/15] selftests: xsk: factor endpoint work out of pthread wrappers Maciej Fijalkowski
                   ` (14 more replies)
  0 siblings, 15 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

v3: https://lore.kernel.org/netdev/20261014184539.774708-1-maciej.fijalkowski@intel.com/
v3 -> v4:
- add tags from Mr Björn
- Drop --hw in xskxceiver, align packet layout between hw/veth runners (Björn)
- drop parse_u32() in xskxceiver (Björn)
- move ethtool -G and ring sizing logic from C to python
- configurable --timeout (Björn)
- keep the HW_SW_MAX_RING_SIZE batch below the XSK TX ring size (Sashiko)
- restore hugepage count in veth runner on exception paths (Sashiko)
- use ExitStack in hw runner to cover setup unrolls (Björn, Sashiko)
- support IPv6 next to IPv4 (Björn)
- send to the next-hop MAC, the peer need not be on-link (Björn)
- make --listen work on its own rather than follow the TX role (Björn)
- run XDP_SHARED_UMEM on HW, the socket is picked by UDP source port
- fail a case when one endpoint fails and the other skips (Björn)
- keep the TX half of the unaligned check in the xsk_caps conversion
- detach in cleanup_iface() with the flags of mode_to_xdp_flags()
- reserve 2 MiB huge pages, tolerate hosts without hugetlb or root (Sashiko)
- lower the veth case timeout to 20s, within the net timeout (Sashiko)
- guard pkts_in_flight against forged PROGRESS reports (Sashiko)
- log short fragments, fix the IPv4 fragment comment (Sashiko)
- keep the MIN_PKT_SIZE and PKT_HDR_SIZE names to cut churn
- read what a gone peer left at READY, drain after a SKIP (Sashiko)
- cap --max-frags, which sizes a packet array (Sashiko)
- skip the hw run when RSS or ntuple setup is refused (Sashiko)
- bound the gateway ping, fail clearly without its MAC (Sashiko)
- note in the README that RFS has to stay off on the DUT (Sashiko)

v2: https://lore.kernel.org/netdev/20261008114909.734364-1-maciej.fijalkowski@intel.com/
v2 -> v3:
- new patch: size the TX UMEM of HW_SW_MAX_RING_SIZE like the RX one
- replace test_xsk.sh with test_xsk.py, a veth pair per case (Sashiko)
- add busy-poll variants to both runners
- wait for the listener's stderr line instead of polling with ss
- default MTU of 1500, wait for link after MTU/XDP changes (Sashiko)
- fill the fill ring before bind, as zero-copy drivers need
- xskxceiver -L lists HW cases, replacing test_xsk_case_defs.h (Jakub)
- run more cases on HW, e.g. TEARDOWN and HW_SW_{MIN,MAX}_RING_SIZE
- follow drv-net conventions in xsk.py, drop XSK_* variables (Jakub)
- set up queue, ntuple and huge pages once per run
- build, ring reset, control channel, XDP and xsk.py fixes (Sashiko)
- document both runners with a netns remote example

v1: https://lore.kernel.org/netdev/20261001202124.692591-1-maciej.fijalkowski@intel.com/
v1 -> v2:
- rebase on net-next, NIPA could not apply v1 (Stanislav)
- new patch: pass non-test traffic to the stack on HW (Stanislav)
- support a netns remote, like NIPA's E810/X710 setup (Stanislav)
- drop XSK_STATIC, link xskxceiver dynamically
- fail TX when RX exits with packets in flight
- fix IPv4/UDP lengths in verbatim multi-buffer streams
- build fixes for a failing or broken bpftool
- document both remote types and the traffic filter


Hi,

This work moves the AF_XDP test suite over to selftests/net and adds
a hardware test on top of the python-based drv-net infrastructure.
Since a non-zero effort went into implementing xskxceiver's (not so
great testing app name) test cases, we did not want to completely
abandon it and start everything from scratch within different infra.
However, hooking it up to the networking CI will allow us to run
cyclic tests on real HW; before that, all of our HW tests were manual
local runs.

Tests were based on a process with two threads responsible for the RX
and TX paths, whereas the new infra expects two separate processes for
the DUT and remote side, where each side has either rx or tx role.

To satisfy this requirement, this series makes each xskxceiver endpoint
a process of its own, so that the RX and TX sides of a case can run on
different hosts, and adds drivers/net/hw/xsk.py, which runs the existing
test cases with the DUT in zero-copy mode against an SKB-mode xskxceiver
on a remote host or on the peer port of the same host in a network
namespace. The veth test moves with the engine from selftests/bpf to
selftests/net as test_xsk.py. It replaces test_xsk.sh and runs all of
its cases but XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF, which leaks page_pool
pages on veth, and XDP_ADJUST_TAIL_GROW, as veth leaves no room to grow
most frames.

ZC tests used to expect a single NIC in loopback mode, with both
sockets on one of its queues sharing a UMEM. Now we step away from it:
the DUT and the remote are separate interfaces, on two hosts or on one
host with the remote in a namespace, and an ntuple rule steers
the test traffic to the AF_XDP queue.

The remote endpoint is xskxceiver in SKB mode rather than a plain
socket, so both ends keep sharing the packet stream generation and
validation of the existing cases. This reduces the need for remote
interface being a NIC from narrow set of NICs that are AF_XDP ZC
capable.

This implies that during the test run only one side is actually
exercised, so let's introduce the concept of direction per test case.
For example, this means rx_SEND_RECEIVE will test the
ice_clean_rx_irq_zc() routine and tx_SEND_RECEIVE the ice_xmit_zc().

BPF's 'test_progs -t xsk' is removed, as well as single interface mode,
which was used for ZC tests. BPF CI therefore no longer runs the
xskxceiver cases. The veth test is kept, as it is the only run that
needs no hardware and not all tests are currently covered by the HW
test side.
We can decide whether to keep the delta test cases, drop them or somehow
enable within HW tests.

Thread-based approach had a pacing mechanism that was a simple in-flight
packet counter updated within critical section by both ends.
Process-based way now is going to do this pacing via xsk_peer.

xsk_peer, the control channel, carries four fixed-size messages: READY
is the barrier between steps, PROGRESS tells TX how many packets RX has
consumed so that TX does not overrun the RX UMEM, ABORT stops the peer
after a failure, and SKIP makes the peer skip a case that its endpoint
skips.

The test table in test_xsk.h is grouped by the endpoint role that each
case tests, which a hardware run gives to the DUT: RX, TX, both, or
none for the generic cases, which run on veth only. xskxceiver -L lists
each hardware case once per role, and xsk.py makes the variants
ksft_variants() named rx_<case> and tx_<case> from that list, so -t,
-T and -l work as for any other test.


Patches 1-4 prepare the split. Patch 1 moves the endpoint work out of
the pthread entry points. Patch 2 sizes the TX UMEM of
HW_SW_MAX_RING_SIZE like the RX one, as the case only worked with the
UMEM that the single-interface mode shares. Patches 3 and 4 drop the
single-interface loopback mode and the test_progs wrapper, which runs
both endpoints as threads of test_progs; neither can work with one
endpoint per process. Nothing else runs a subset of the cases, so
patch 4 also merges the cases that the wrapper left out into the main
list.

Patch 5 adds a generic rule for BPF skeletons to net/bpf.mk, as Jakub
suggested in the review of the xdp_features move [1]; xskxceiver is
its first user. If that series lands first with the same rule, this
patch can be dropped.

Patch 6 moves the engine, its XDP program and the veth launcher to
selftests/net. xsk.py needs xskxceiver, and a drv-net test can only
rely on net/lib: the selftests build pulls net/lib in for net,
drivers/net and drivers/net/hw, while it skips selftests/bpf by
default. The veth test is software-only, so it goes to selftests/net,
as the drv-net README asks. selftests/bpf keeps building xsk.c from
its new place for xdp_hw_metadata and the xdp_metadata test.

Patches 7-10 split the engine. The interface capabilities move into one
struct, so that a process can mirror them for the endpoint it does not
own (7). main() is split into setup, run and cleanup (8). xskxceiver
runs one case per invocation, and test_xsk.py, which replaces
test_xsk.sh, owns the mode x case matrix (9). Finally, the RX and TX
endpoints become separate processes that meet over a small TCP control
channel (10).

Patch 11 makes the endpoints send UDP over IPv4 or IPv6 between the
interface MACs, on veth as on hardware, and lets the XDP programs pass
all other traffic to the stack. Patch 12 adds what endpoints on two
hosts need, and patch 13 groups the test table by endpoint role and
lists the hardware cases with xskxceiver -L. Patch 14 adds xsk.py on
top of them, and patch 15 documents both setups.


Known issues:
- Every case pays for process start-up, XDP attach and detach and, on
  hardware, its remote commands. We used to configure resources once
  and then execute the whole test suite; it doesn't seem to be
  CI-friendly and it is preferred to have each case's resource
  management separated; that on the other hand increases the
  execution time of the whole test suite. Only what all cases share,
  the huge pages and, on hardware, the queue taken out of RSS and the
  ntuple rules, is set up once per run.

- The control channel goes to the remote address with either remote
  type, so it shares the tested link. A zero-copy TX socket fills no
  RX buffers and its queue would drop that traffic, so the DUT queue
  stays out of RSS in both directions, and a DUT with fewer than two
  queues skips the run.

- The control channel is unauthenticated TCP, and the remote endpoint
  listens on the remote address while its case runs.

- Both endpoints get only the case number, and the control channel
  does not check that they run the same case. xsk.py copies the DUT's
  xskxceiver to the remote with cfg.remote.deploy(), so that both ends
  number the cases alike.

- Not on hardware (yet, or ever):
 * BIDIRECTIONAL, which needs both directions set up in one case; i
   think it should be removed as we sort of achieve the bi-directional
   coverage via the rx_ and tx_ variants, and it currently does not fit
   our KISS-policy on remote side;
 * STAT_RX_DROPPED and TX_QUEUE_CONSUMER, which do not run in zero-copy
   mode.

[1]: https://lore.kernel.org/netdev/20260925161547.1b57b653@kernel.org/

Thanks,
Maciej

Maciej Fijalkowski (15):
  selftests: xsk: factor endpoint work out of pthread wrappers
  selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE
  selftests: xsk: drop the single-interface loopback mode
  selftests/bpf: drop the test_progs AF_XDP wrapper
  selftests: net: add a generic rule for BPF skeletons
  selftests: xsk: move the AF_XDP test suite to selftests/net
  selftests: xsk: collect interface capabilities in struct xsk_caps
  selftests: xsk: split xskxceiver main() into setup, run and cleanup
  selftests: xsk: run one test case per xskxceiver invocation
  selftests: xsk: run the RX and TX endpoints in separate processes
  selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs
  selftests: xsk: prepare xskxceiver for endpoints on two hosts
  selftests: xsk: list the hardware test cases
  selftests: drv-net: test AF_XDP zero-copy with an SKB peer
  selftests: xsk: document generic and hardware endpoint runs

 Documentation/networking/af_xdp.rst           |   6 +-
 MAINTAINERS                                   |   5 +-
 tools/testing/selftests/bpf/.gitignore        |   1 -
 tools/testing/selftests/bpf/Makefile          |  29 +-
 tools/testing/selftests/bpf/network_helpers.c |  48 -
 tools/testing/selftests/bpf/network_helpers.h |   2 -
 tools/testing/selftests/bpf/prog_tests/xsk.c  | 170 ----
 tools/testing/selftests/bpf/test_xsk.sh       | 246 -----
 tools/testing/selftests/bpf/xsk_prereqs.sh    |  93 --
 tools/testing/selftests/bpf/xskxceiver.c      | 479 ----------
 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 tools/testing/selftests/drivers/net/hw/config |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   5 +
 tools/testing/selftests/drivers/net/hw/xsk.py | 368 ++++++++
 tools/testing/selftests/net/Makefile          |   1 +
 tools/testing/selftests/net/bpf.mk            |   6 +
 tools/testing/selftests/net/config            |   1 +
 tools/testing/selftests/net/lib/.gitignore    |   2 +
 tools/testing/selftests/net/lib/Makefile      |  34 +
 .../testing/selftests/net/lib/py/__init__.py  |   4 +
 tools/testing/selftests/net/lib/py/xsk.py     |  96 ++
 .../testing/selftests/net/lib/xsk/README.rst  | 194 ++++
 .../prog_tests => net/lib/xsk}/test_xsk.c     | 876 +++++++++++-------
 .../prog_tests => net/lib/xsk}/test_xsk.h     | 155 +++-
 .../selftests/{bpf => net/lib/xsk}/xsk.c      |  85 +-
 .../selftests/{bpf => net/lib/xsk}/xsk.h      |   6 +
 .../testing/selftests/net/lib/xsk/xsk_peer.c  | 339 +++++++
 .../testing/selftests/net/lib/xsk/xsk_peer.h  |  21 +
 .../{bpf => net/lib/xsk}/xsk_xdp_common.h     |   3 +-
 .../lib/xsk/xsk_xdp_progs.bpf.c}              |  91 +-
 .../selftests/net/lib/xsk/xskxceiver.c        | 620 +++++++++++++
 .../{bpf => net/lib/xsk}/xskxceiver.h         |   0
 tools/testing/selftests/net/test_xsk.py       | 163 ++++
 33 files changed, 2697 insertions(+), 1454 deletions(-)
 delete mode 100644 tools/testing/selftests/bpf/prog_tests/xsk.c
 delete mode 100755 tools/testing/selftests/bpf/test_xsk.sh
 delete mode 100755 tools/testing/selftests/bpf/xsk_prereqs.sh
 delete mode 100644 tools/testing/selftests/bpf/xskxceiver.c
 create mode 100755 tools/testing/selftests/drivers/net/hw/xsk.py
 create mode 100644 tools/testing/selftests/net/lib/py/xsk.py
 create mode 100644 tools/testing/selftests/net/lib/xsk/README.rst
 rename tools/testing/selftests/{bpf/prog_tests => net/lib/xsk}/test_xsk.c (79%)
 rename tools/testing/selftests/{bpf/prog_tests => net/lib/xsk}/test_xsk.h (78%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk.c (90%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk.h (96%)
 create mode 100644 tools/testing/selftests/net/lib/xsk/xsk_peer.c
 create mode 100644 tools/testing/selftests/net/lib/xsk/xsk_peer.h
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk_xdp_common.h (67%)
 rename tools/testing/selftests/{bpf/progs/xsk_xdp_progs.c => net/lib/xsk/xsk_xdp_progs.bpf.c} (57%)
 create mode 100644 tools/testing/selftests/net/lib/xsk/xskxceiver.c
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xskxceiver.h (100%)
 create mode 100755 tools/testing/selftests/net/test_xsk.py

-- 
2.43.0


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 01/15] selftests: xsk: factor endpoint work out of pthread wrappers
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE Maciej Fijalkowski
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Move TX execution, RX setup and RX validation into synchronous helpers
and leave the pthread entry points as thin wrappers, so that the same
code can run in a process per endpoint later in this series. Barrier,
pacing and test behavior are unchanged.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../selftests/bpf/prog_tests/test_xsk.c       | 98 +++++++++++--------
 1 file changed, 59 insertions(+), 39 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
index 4549358cc8c2..2a1362874092 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
@@ -1623,40 +1623,31 @@ static int thread_common_ops(struct test_spec *test, struct ifobject *ifobject)
 	return 0;
 }
 
-void *worker_testapp_validate_tx(void *arg)
+static int testapp_validate_tx_endpoint(struct test_spec *test,
+					struct ifobject *ifobject)
 {
-	struct test_spec *test = (struct test_spec *)arg;
-	struct ifobject *ifobject = test->ifobj_tx;
 	int err;
 
 	if (test->current_step == 1) {
-		if (!ifobject->shared_umem) {
-			if (thread_common_ops(test, ifobject)) {
-				test->fail = true;
-				pthread_exit(NULL);
-			}
-		} else {
-			if (thread_common_ops_tx(test, ifobject)) {
-				test->fail = true;
-				pthread_exit(NULL);
-			}
-		}
+		if (!ifobject->shared_umem)
+			err = thread_common_ops(test, ifobject);
+		else
+			err = thread_common_ops_tx(test, ifobject);
+		if (err)
+			return err;
 	}
 
 	err = send_pkts(test, ifobject);
 
 	if (!err && ifobject->validation_func)
 		err = ifobject->validation_func(ifobject);
-	if (err)
-		test->fail = true;
 
-	pthread_exit(NULL);
+	return err;
 }
 
-void *worker_testapp_validate_rx(void *arg)
+static int testapp_prepare_rx_endpoint(struct test_spec *test,
+				       struct ifobject *ifobject)
 {
-	struct test_spec *test = (struct test_spec *)arg;
-	struct ifobject *ifobject = test->ifobj_rx;
 	int err;
 
 	if (test->current_step == 1) {
@@ -1669,35 +1660,64 @@ void *worker_testapp_validate_rx(void *arg)
 				       strerror(-err));
 	}
 
-	if (test->use_barrier)
-		pthread_barrier_wait(&barr);
+	return err;
+}
 
-	/* We leave only now in case of error to avoid getting stuck in the barrier */
-	if (err) {
-		test->fail = true;
-		pthread_exit(NULL);
-	}
+static int testapp_validate_rx_endpoint(struct test_spec *test,
+					struct ifobject *ifobject)
+{
+	bool supported;
+	int err;
 
 	err = receive_pkts(test);
 
 	if (!err && ifobject->validation_func)
 		err = ifobject->validation_func(ifobject);
+	if (!err)
+		return TEST_PASS;
 
-	if (err) {
-		if (!test->adjust_tail) {
-			test->fail = true;
-		} else {
-			bool supported;
+	if (!test->adjust_tail)
+		return err;
 
-			if (is_adjust_tail_supported(ifobject->xdp_progs, &supported))
-				test->fail = true;
-			else if (!supported)
-				test->adjust_tail_support = false;
-			else
-				test->fail = true;
-		}
+	if (is_adjust_tail_supported(ifobject->xdp_progs, &supported))
+		return TEST_FAILURE;
+	if (!supported) {
+		test->adjust_tail_support = false;
+		return TEST_PASS;
 	}
 
+	return err;
+}
+
+void *worker_testapp_validate_tx(void *arg)
+{
+	struct test_spec *test = (struct test_spec *)arg;
+	int err;
+
+	err = testapp_validate_tx_endpoint(test, test->ifobj_tx);
+	if (err)
+		test->fail = true;
+
+	pthread_exit(NULL);
+}
+
+void *worker_testapp_validate_rx(void *arg)
+{
+	struct test_spec *test = (struct test_spec *)arg;
+	struct ifobject *ifobject = test->ifobj_rx;
+	int err;
+
+	err = testapp_prepare_rx_endpoint(test, ifobject);
+
+	if (test->use_barrier)
+		pthread_barrier_wait(&barr);
+
+	/* We leave only now in case of error to avoid getting stuck in the barrier */
+	if (!err)
+		err = testapp_validate_rx_endpoint(test, ifobject);
+	if (err)
+		test->fail = true;
+
 	pthread_exit(NULL);
 }
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 01/15] selftests: xsk: factor endpoint work out of pthread wrappers Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-17 17:44   ` sashiko-bot
  2026-10-16 17:43 ` [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode Maciej Fijalkowski
                   ` (12 subsequent siblings)
  14 siblings, 1 reply; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

HW_SW_MAX_RING_SIZE gives the RX UMEM 8k frames and 8k rings, then
sends batches of 2k and of the maximum HW TX ring size minus 8. This
only works in the single-interface mode, where the TX socket uses the
RX UMEM, and veth, which cannot change its ring sizes, skips the case.
With two interfaces, TX has its own UMEM of DEFAULT_UMEM_BUFFERS (1k)
frames and 2k rings: the in-flight limit in __send_pkts() turns
negative for a 2k batch, so TX never sends and RX times out, and the
second batch would not fit in the TX ring anyway.

The next patch removes the single-interface mode, so size the TX UMEM
like the RX one. The shared UMEM setup copies the RX UMEM over the TX
one and is not affected.

With a HW TX ring of more than 8200 descriptors, the second batch would
also outgrow the 8k XSK TX ring, and __send_pkts() would spin forever
waiting to reserve it, so cap the batch below the XSK ring size.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 tools/testing/selftests/bpf/prog_tests/test_xsk.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
index 2a1362874092..05457f77ca52 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
@@ -2504,6 +2504,7 @@ int testapp_hw_sw_min_ring_size(struct test_spec *test)
 int testapp_hw_sw_max_ring_size(struct test_spec *test)
 {
 	u32 max_descs = XSK_RING_PROD__DEFAULT_NUM_DESCS * 4;
+	u32 batch_size;
 	int ret;
 
 	test->set_ring = true;
@@ -2513,6 +2514,9 @@ int testapp_hw_sw_max_ring_size(struct test_spec *test)
 	test->ifobj_rx->xsk->umem->num_frames = max_descs;
 	test->ifobj_rx->xsk->umem->fill_size = max_descs;
 	test->ifobj_rx->xsk->umem->comp_size = max_descs;
+	test->ifobj_tx->xsk->umem->num_frames = max_descs;
+	test->ifobj_tx->xsk->umem->fill_size = max_descs;
+	test->ifobj_tx->xsk->umem->comp_size = max_descs;
 	test->ifobj_tx->xsk->batch_size = XSK_RING_PROD__DEFAULT_NUM_DESCS;
 	test->ifobj_rx->xsk->batch_size = XSK_RING_PROD__DEFAULT_NUM_DESCS;
 
@@ -2521,10 +2525,12 @@ int testapp_hw_sw_max_ring_size(struct test_spec *test)
 		return ret;
 
 	/* Set batch_size to 8152 for testing, as the ice HW ignores the 3 lowest bits when
-	 * updating the Rx HW tail register.
+	 * updating the Rx HW tail register. __send_pkts() waits until a whole batch fits
+	 * in the XSK TX ring, so keep it below that ring's size on NICs with larger rings.
 	 */
-	test->ifobj_tx->xsk->batch_size = test->ifobj_tx->ring.tx_max_pending - 8;
-	test->ifobj_rx->xsk->batch_size = test->ifobj_tx->ring.tx_max_pending - 8;
+	batch_size = min(test->ifobj_tx->ring.tx_max_pending, max_descs) - 8;
+	test->ifobj_tx->xsk->batch_size = batch_size;
+	test->ifobj_rx->xsk->batch_size = batch_size;
 	if (pkt_stream_replace(test, max_descs, MIN_PKT_SIZE)) {
 		clean_sockets(test, test->ifobj_tx);
 		clean_sockets(test, test->ifobj_rx);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 01/15] selftests: xsk: factor endpoint work out of pthread wrappers Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-17 17:44   ` sashiko-bot
  2026-10-16 17:43 ` [PATCH v4 net-next 04/15] selftests/bpf: drop the test_progs AF_XDP wrapper Maciej Fijalkowski
                   ` (11 subsequent siblings)
  14 siblings, 1 reply; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

xskxceiver -i ethX -i ethX, used for HW tests, bound both sockets to
queue 0 of one interface, which the kernel only allows with a shared
UMEM. With the endpoints about to become separate processes, they would
have to share the UMEM mapping and the socket fd.

The mode also relied on the interface being set up by hand with a
single queue and promiscuous mode, which does not suit CI. The hardware
test added later in this series steers its traffic with ntuple and RSS
configuration instead.

Remove the shared-UMEM TX setup, the split UMEM addressing and the
POLL_TXQ_FULL workaround it needed, and drop the -i IFACE option of
test_xsk.sh that used it. XDP_SHARED_UMEM (two sockets of one endpoint)
is unaffected.

test_xsk.sh ran HW_SW_MIN_RING_SIZE and HW_SW_MAX_RING_SIZE only in
this mode, as veth has no ring parameters. xskxceiver resized the rings
for them and restored the sizes it had found only when it exited, so a
killed run left the test sizes behind for the next run to take as the
defaults. Drop the resizing with the mode, and set_hw_ring_size() with
it, as nothing else uses it: the hardware test sizes the rings with
ethtool -G and restores them after each case, however the endpoints
end. Both cases still skip on interfaces without ring parameters, and
HW_SW_MAX_RING_SIZE still reads the maximum TX ring size for its
batches.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 tools/testing/selftests/bpf/network_helpers.c |  24 ---
 tools/testing/selftests/bpf/network_helpers.h |   1 -
 .../selftests/bpf/prog_tests/test_xsk.c       | 162 ++++--------------
 .../selftests/bpf/prog_tests/test_xsk.h       |  12 --
 tools/testing/selftests/bpf/prog_tests/xsk.c  |  12 +-
 tools/testing/selftests/bpf/test_xsk.sh       |  49 ++----
 tools/testing/selftests/bpf/xsk_prereqs.sh    |   7 -
 tools/testing/selftests/bpf/xskxceiver.c      |  13 +-
 8 files changed, 51 insertions(+), 229 deletions(-)

diff --git a/tools/testing/selftests/bpf/network_helpers.c b/tools/testing/selftests/bpf/network_helpers.c
index cdf2d7d3ab32..888f03609175 100644
--- a/tools/testing/selftests/bpf/network_helpers.c
+++ b/tools/testing/selftests/bpf/network_helpers.c
@@ -646,30 +646,6 @@ int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param)
 	return 0;
 }
 
-int set_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param)
-{
-	struct ifreq ifr = {0};
-	int sockfd, err;
-
-	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
-	if (sockfd < 0)
-		return -errno;
-
-	memcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
-
-	ring_param->cmd = ETHTOOL_SRINGPARAM;
-	ifr.ifr_data = (char *)ring_param;
-
-	if (ioctl(sockfd, SIOCETHTOOL, &ifr) < 0) {
-		err = errno;
-		close(sockfd);
-		return -err;
-	}
-
-	close(sockfd);
-	return 0;
-}
-
 struct send_recv_arg {
 	int		fd;
 	uint32_t	bytes;
diff --git a/tools/testing/selftests/bpf/network_helpers.h b/tools/testing/selftests/bpf/network_helpers.h
index 75133119c04a..c7c52597c33f 100644
--- a/tools/testing/selftests/bpf/network_helpers.h
+++ b/tools/testing/selftests/bpf/network_helpers.h
@@ -90,7 +90,6 @@ int make_sockaddr(int family, const char *addr_str, __u16 port,
 char *ping_command(int family);
 int get_socket_local_port(int sock_fd);
 int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
-int set_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
 
 int open_tuntap(const char *dev_name, bool need_mac);
 
diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
index 05457f77ca52..b9126fe66e1a 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
@@ -101,10 +101,6 @@ int xsk_configure_umem(struct ifobject *ifobj, struct xsk_umem_info *umem, void
 		return ret;
 
 	umem->buffer = buffer;
-	if (ifobj->shared_umem && ifobj->rx_on) {
-		umem->base_addr = umem_size(umem);
-		umem->next_buffer = umem_size(umem);
-	}
 
 	return 0;
 }
@@ -115,8 +111,8 @@ static u64 umem_alloc_buffer(struct xsk_umem_info *umem)
 
 	addr = umem->next_buffer;
 	umem->next_buffer += umem->frame_size;
-	if (umem->next_buffer >= umem->base_addr + umem_size(umem))
-		umem->next_buffer = umem->base_addr;
+	if (umem->next_buffer >= umem_size(umem))
+		umem->next_buffer = 0;
 
 	return addr;
 }
@@ -173,33 +169,6 @@ int xsk_configure_socket(struct xsk_socket_info *xsk, struct xsk_umem_info *umem
 	return xsk_socket__create(&xsk->xsk, ifobject->ifindex, 0, umem->umem, rxr, txr, &cfg);
 }
 
-static int set_ring_size(struct ifobject *ifobj)
-{
-	int ret;
-	u32 ctr = 0;
-
-	while (ctr++ < SOCK_RECONF_CTR) {
-		ret = set_hw_ring_size(ifobj->ifname, &ifobj->ring);
-		if (!ret)
-			break;
-
-		/* Retry if it fails */
-		if (ctr >= SOCK_RECONF_CTR || errno != EBUSY)
-			return -errno;
-
-		usleep(USLEEP_MAX);
-	}
-
-	return ret;
-}
-
-int hw_ring_size_reset(struct ifobject *ifobj)
-{
-	ifobj->ring.tx_pending = ifobj->set_ring.default_tx;
-	ifobj->ring.rx_pending = ifobj->set_ring.default_rx;
-	return set_ring_size(ifobj);
-}
-
 static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 			     struct ifobject *ifobj_rx)
 {
@@ -207,7 +176,7 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 
 	for (i = 0; i < MAX_INTERFACES; i++) {
 		struct ifobject *ifobj = i ? ifobj_rx : ifobj_tx;
-		struct xsk_umem_info *umem_real;
+		struct xsk_umem_info *umem;
 
 		ifobj->xsk = &ifobj->xsk_arr[0];
 		ifobj->use_poll = false;
@@ -224,16 +193,14 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 			ifobj->tx_on = false;
 		}
 
-		umem_real = ifobj->xsk_arr[0].umem_real;
-		memset(umem_real, 0, sizeof(*umem_real));
+		umem = ifobj->xsk_arr[0].umem;
+		memset(umem, 0, sizeof(*umem));
 		for (j = 0; j < MAX_SOCKETS; j++) {
 			struct xsk_socket_info *xsk = &ifobj->xsk_arr[j];
 
 			memset(xsk, 0, sizeof(*xsk));
 			xsk->rxqsize = XSK_RING_CONS__DEFAULT_NUM_DESCS;
-			if (j == 0)
-				xsk->umem_real = umem_real;
-			xsk->umem = umem_real;
+			xsk->umem = umem;
 			xsk->batch_size = DEFAULT_BATCH_SIZE;
 			if (i == 0)
 				xsk->pkt_stream = test->tx_pkt_stream_default;
@@ -250,16 +217,12 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 		ifobj->xsk->umem->frame_size = XSK_UMEM__DEFAULT_FRAME_SIZE;
 	}
 
-	if (ifobj_tx->hw_ring_size_supp)
-		hw_ring_size_reset(ifobj_tx);
-
 	test->ifobj_tx = ifobj_tx;
 	test->ifobj_rx = ifobj_rx;
 	test->current_step = 0;
 	test->total_steps = 1;
 	test->nb_sockets = 1;
 	test->fail = false;
-	test->set_ring = false;
 	test->adjust_tail = false;
 	test->adjust_tail_support = false;
 	test->mtu = MAX_ETH_PKT_SIZE;
@@ -825,8 +788,6 @@ static bool is_frag_valid(struct xsk_umem_info *umem, u64 addr, u32 len, u32 exp
 	void *data = xsk_umem__get_data(umem->buffer, addr);
 	u64 umem_sz = umem_size(umem);
 
-	addr -= umem->base_addr;
-
 	if (addr >= umem_sz || addr + len > umem_sz) {
 		ksft_print_msg("Frag invalid addr: %llx len: %u\n",
 			       (unsigned long long)addr, len);
@@ -1164,7 +1125,7 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 		       bool test_timeout)
 {
 	u32 i, idx = 0, valid_pkts = 0, valid_frags = 0, buffer_len;
-	struct xsk_umem_info *umem = ifobject->xsk_arr[0].umem_real;
+	struct xsk_umem_info *umem = ifobject->xsk_arr[0].umem;
 	struct pkt_stream *pkt_stream = xsk->pkt_stream;
 	bool use_poll = ifobject->use_poll;
 	struct pollfd fds = { };
@@ -1468,12 +1429,12 @@ static int validate_tx_invalid_descs(struct ifobject *ifobject)
 }
 
 static int xsk_configure(struct test_spec *test, struct ifobject *ifobject,
-			  struct xsk_umem_info *umem, bool tx)
+			  struct xsk_umem_info *umem)
 {
 	int i, ret;
 
 	for (i = 0; i < test->nb_sockets; i++) {
-		bool shared = (ifobject->shared_umem && tx) ? true : !!i;
+		bool shared = !!i;
 		u32 ctr = 0;
 
 		while (ctr++ < SOCK_RECONF_CTR) {
@@ -1497,31 +1458,6 @@ static int xsk_configure(struct test_spec *test, struct ifobject *ifobject,
 	return 0;
 }
 
-static int thread_common_ops_tx(struct test_spec *test, struct ifobject *ifobject)
-{
-	struct xsk_umem_info *umem_rx, *umem_tx;
-	int ret;
-
-	if (!test->ifobj_rx || !test->ifobj_rx->xsk_arr[0].umem->umem) {
-		ksft_print_msg("Error: RX UMEM is not initialized before shared-UMEM TX setup\n");
-		return -EINVAL;
-	}
-
-	umem_rx = test->ifobj_rx->xsk_arr[0].umem;
-	umem_tx = ifobject->xsk_arr[0].umem_real;
-	memcpy(umem_tx, umem_rx, sizeof(*umem_tx));
-	umem_tx->base_addr = 0;
-	umem_tx->next_buffer = 0;
-
-	ret = xsk_configure(test, ifobject, umem_rx, true);
-	if (ret)
-		return ret;
-	ifobject->xsk = &ifobject->xsk_arr[0];
-	ifobject->xskmap = test->ifobj_rx->xskmap;
-
-	return 0;
-}
-
 static int xsk_populate_fill_ring(struct xsk_umem_info *umem, struct pkt_stream *pkt_stream,
 				   bool fill_up)
 {
@@ -1547,7 +1483,7 @@ static int xsk_populate_fill_ring(struct xsk_umem_info *umem, struct pkt_stream
 			if (!pkt) {
 				if (!fill_up)
 					break;
-				addr = filled * umem->frame_size + umem->base_addr;
+				addr = filled * umem->frame_size;
 			} else if (pkt->offset >= 0) {
 				addr = pkt->offset % umem->frame_size + umem_alloc_buffer(umem);
 			} else {
@@ -1584,9 +1520,6 @@ static int thread_common_ops(struct test_spec *test, struct ifobject *ifobject)
 	if (umem->unaligned_mode)
 		mmap_flags |= MAP_HUGETLB | MAP_HUGE_2MB;
 
-	if (ifobject->shared_umem)
-		umem_sz *= 2;
-
 	mmap_sz = umem->unaligned_mode ?
 		ceil_u64(umem_sz, HUGEPAGE_SIZE) * HUGEPAGE_SIZE : umem_sz;
 
@@ -1600,7 +1533,7 @@ static int thread_common_ops(struct test_spec *test, struct ifobject *ifobject)
 	if (ret)
 		return ret;
 
-	ret = xsk_configure(test, ifobject, umem, false);
+	ret = xsk_configure(test, ifobject, umem);
 	if (ret)
 		return ret;
 
@@ -1629,10 +1562,7 @@ static int testapp_validate_tx_endpoint(struct test_spec *test,
 	int err;
 
 	if (test->current_step == 1) {
-		if (!ifobject->shared_umem)
-			err = thread_common_ops(test, ifobject);
-		else
-			err = thread_common_ops_tx(test, ifobject);
+		err = thread_common_ops(test, ifobject);
 		if (err)
 			return err;
 	}
@@ -1779,7 +1709,7 @@ static int xsk_attach_xdp_progs(struct test_spec *test, struct ifobject *ifobj_r
 			return err;
 	}
 
-	if (!ifobj_tx || ifobj_tx->shared_umem)
+	if (!ifobj_tx)
 		return 0;
 
 	if (xdp_prog_changed_tx(test))
@@ -1805,7 +1735,7 @@ static void clean_umem(struct test_spec *test, struct ifobject *ifobj1, struct i
 		return;
 
 	testapp_clean_xsk_umem(ifobj1);
-	if (ifobj2 && !ifobj2->shared_umem)
+	if (ifobj2)
 		testapp_clean_xsk_umem(ifobj2);
 }
 
@@ -1897,18 +1827,6 @@ static int testapp_validate_traffic(struct test_spec *test)
 		return TEST_SKIP;
 	}
 
-	if (test->set_ring) {
-		if (ifobj_tx->hw_ring_size_supp) {
-			if (set_ring_size(ifobj_tx)) {
-				ksft_print_msg("Failed to change HW ring size.\n");
-				return TEST_FAILURE;
-			}
-		} else {
-			ksft_print_msg("Changing HW ring size not supported.\n");
-			return TEST_SKIP;
-		}
-	}
-
 	return __testapp_validate_traffic(test, ifobj_rx, ifobj_tx);
 }
 
@@ -2184,12 +2102,6 @@ int testapp_invalid_desc(struct test_spec *test)
 		pkts[8].valid = false;
 	}
 
-	if (test->ifobj_tx->shared_umem) {
-		pkts[4].offset += umem_sz;
-		pkts[5].offset += umem_sz;
-		pkts[6].offset += umem_sz;
-	}
-
 	if (pkt_stream_generate_custom(test, pkts, ARRAY_SIZE(pkts)))
 		return TEST_FAILURE;
 	return testapp_validate_traffic(test);
@@ -2248,28 +2160,14 @@ int testapp_xdp_shared_umem(struct test_spec *test)
 
 int testapp_poll_txq_tmout(struct test_spec *test)
 {
-	bool shared_umem = test->ifobj_tx->shared_umem;
-	int ret;
-
 	test->poll_tmout = true;
-	/*
-	 * POLL_TXQ_FULL exercises TX timeout setup in isolation.
-	 * Keep TX out of shared-UMEM mode here so TX setup does not require
-	 * RX UMEM to be initialized first.
-	 */
-	test->ifobj_tx->shared_umem = false;
 	test->ifobj_tx->use_poll = true;
 	/* create invalid frame by set umem frame_size and pkt length equal to 2048 */
 	test->ifobj_tx->xsk->umem->frame_size = 2048;
-	if (pkt_stream_replace(test, 2 * DEFAULT_PKT_CNT, 2048)) {
-		test->ifobj_tx->shared_umem = shared_umem;
+	if (pkt_stream_replace(test, 2 * DEFAULT_PKT_CNT, 2048))
 		return TEST_FAILURE;
-	}
 
-	ret = testapp_validate_traffic_single_thread(test, test->ifobj_tx);
-	test->ifobj_tx->shared_umem = shared_umem;
-
-	return ret;
+	return testapp_validate_traffic_single_thread(test, test->ifobj_tx);
 }
 
 int testapp_poll_rxq_tmout(struct test_spec *test)
@@ -2481,14 +2379,19 @@ int testapp_xdp_metadata_mb(struct test_spec *test)
 	return testapp_xdp_metadata_copy(test);
 }
 
+/* Expects the HW TX ring of the sender to hold DEFAULT_BATCH_SIZE
+ * descriptors.
+ */
 int testapp_hw_sw_min_ring_size(struct test_spec *test)
 {
 	int ret;
 
-	test->set_ring = true;
+	if (!test->ifobj_tx->hw_ring_size_supp) {
+		ksft_print_msg("HW ring sizes not supported.\n");
+		return TEST_SKIP;
+	}
+
 	test->total_steps = 2;
-	test->ifobj_tx->ring.tx_pending = DEFAULT_BATCH_SIZE;
-	test->ifobj_tx->ring.rx_pending = DEFAULT_BATCH_SIZE * 2;
 	test->ifobj_tx->xsk->batch_size = 1;
 	test->ifobj_rx->xsk->batch_size = 1;
 	ret = testapp_validate_traffic(test);
@@ -2501,16 +2404,21 @@ int testapp_hw_sw_min_ring_size(struct test_spec *test)
 	return testapp_validate_traffic(test);
 }
 
+/* Expects the HW TX ring of the sender and the HW RX ring of the receiver
+ * at their maximum sizes.
+ */
 int testapp_hw_sw_max_ring_size(struct test_spec *test)
 {
 	u32 max_descs = XSK_RING_PROD__DEFAULT_NUM_DESCS * 4;
 	u32 batch_size;
 	int ret;
 
-	test->set_ring = true;
+	if (!test->ifobj_tx->hw_ring_size_supp) {
+		ksft_print_msg("HW ring sizes not supported.\n");
+		return TEST_SKIP;
+	}
+
 	test->total_steps = 2;
-	test->ifobj_tx->ring.tx_pending = test->ifobj_tx->ring.tx_max_pending;
-	test->ifobj_tx->ring.rx_pending  = test->ifobj_tx->ring.rx_max_pending;
 	test->ifobj_rx->xsk->umem->num_frames = max_descs;
 	test->ifobj_rx->xsk->umem->fill_size = max_descs;
 	test->ifobj_rx->xsk->umem->comp_size = max_descs;
@@ -2659,8 +2567,8 @@ struct ifobject *ifobject_create(void)
 	if (!ifobj->xsk_arr)
 		goto out_xsk_arr;
 
-	ifobj->xsk_arr[0].umem_real = calloc(1, sizeof(struct xsk_umem_info));
-	if (!ifobj->xsk_arr[0].umem_real)
+	ifobj->xsk_arr[0].umem = calloc(1, sizeof(struct xsk_umem_info));
+	if (!ifobj->xsk_arr[0].umem)
 		goto out_umem;
 
 	return ifobj;
@@ -2675,7 +2583,7 @@ struct ifobject *ifobject_create(void)
 void ifobject_delete(struct ifobject *ifobj)
 {
 	if (ifobj->xsk_arr)
-		free(ifobj->xsk_arr[0].umem_real);
+		free(ifobj->xsk_arr[0].umem);
 
 	free(ifobj->xsk_arr);
 	free(ifobj);
diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.h b/tools/testing/selftests/bpf/prog_tests/test_xsk.h
index 03753ddc5dcd..33331fbee6f5 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.h
+++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.h
@@ -83,7 +83,6 @@ typedef int (*test_func_t)(struct test_spec *test);
 struct xsk_socket_info {
 	struct xsk_ring_cons rx;
 	struct xsk_ring_prod tx;
-	struct xsk_umem_info *umem_real;
 	struct xsk_umem_info *umem;
 	struct xsk_socket *xsk;
 	struct pkt_stream *pkt_stream;
@@ -108,19 +107,11 @@ struct xsk_umem_info {
 	u32 frame_headroom;
 	void *buffer;
 	u32 frame_size;
-	u32 base_addr;
 	u32 fill_size;
 	u32 comp_size;
 	bool unaligned_mode;
 };
 
-struct set_hw_ring {
-	u32 default_tx;
-	u32 default_rx;
-};
-
-int hw_ring_size_reset(struct ifobject *ifobj);
-
 struct ifobject {
 	char ifname[MAX_INTERFACE_NAME_CHARS];
 	struct xsk_socket_info *xsk;
@@ -131,7 +122,6 @@ struct ifobject {
 	struct bpf_map *xskmap;
 	struct bpf_program *xdp_prog;
 	struct ethtool_ringparam ring;
-	struct set_hw_ring set_ring;
 	enum test_mode mode;
 	int ifindex;
 	int mtu;
@@ -145,7 +135,6 @@ struct ifobject {
 	bool busy_poll;
 	bool use_fill_ring;
 	bool release_rx;
-	bool shared_umem;
 	bool use_metadata;
 	bool unaligned_supp;
 	bool multi_buff_supp;
@@ -204,7 +193,6 @@ struct test_spec {
 	u16 current_step;
 	u16 nb_sockets;
 	bool fail;
-	bool set_ring;
 	bool adjust_tail;
 	bool adjust_tail_support;
 	bool poll_tmout;
diff --git a/tools/testing/selftests/bpf/prog_tests/xsk.c b/tools/testing/selftests/bpf/prog_tests/xsk.c
index 6e2f63ee2a6c..af67e2605818 100644
--- a/tools/testing/selftests/bpf/prog_tests/xsk.c
+++ b/tools/testing/selftests/bpf/prog_tests/xsk.c
@@ -53,10 +53,6 @@ int configure_ifobj(struct ifobject *tx, struct ifobject *rx)
 	if (!ASSERT_OK_FD(tx->ifindex, "get TX ifindex"))
 		return -1;
 
-	tx->shared_umem = false;
-	rx->shared_umem = false;
-
-
 	return 0;
 }
 
@@ -79,11 +75,8 @@ static void test_xsk(const struct test_spec *test_to_run, enum test_mode mode)
 		goto delete_rx;
 
 	ret = get_hw_ring_size(ifobj_tx->ifname, &ifobj_tx->ring);
-	if (!ret) {
+	if (!ret)
 		ifobj_tx->hw_ring_size_supp = true;
-		ifobj_tx->set_ring.default_tx = ifobj_tx->ring.tx_pending;
-		ifobj_tx->set_ring.default_rx = ifobj_tx->ring.rx_pending;
-	}
 
 	cache_line_size = read_procfs_val(SMP_CACHE_BYTES_PATH);
 	if (!cache_line_size)
@@ -124,9 +117,6 @@ static void test_xsk(const struct test_spec *test_to_run, enum test_mode mode)
 		ASSERT_OK(ret, "Run test");
 	pkt_stream_restore_default(&test);
 
-	if (ifobj_tx->hw_ring_size_supp)
-		hw_ring_size_reset(ifobj_tx);
-
 	pkt_stream_delete(test.tx_pkt_stream_default);
 	pkt_stream_delete(test.rx_pkt_stream_default);
 	xsk_xdp_progs__destroy(ifobj_tx->xdp_progs);
diff --git a/tools/testing/selftests/bpf/test_xsk.sh b/tools/testing/selftests/bpf/test_xsk.sh
index 62db060298a4..69fac65f073c 100755
--- a/tools/testing/selftests/bpf/test_xsk.sh
+++ b/tools/testing/selftests/bpf/test_xsk.sh
@@ -71,9 +71,6 @@
 # Set up veth interfaces and leave them up so xskxceiver can be launched in a debugger:
 #   sudo ./test_xsk.sh -d
 #
-# Run test suite for physical device in loopback mode
-#   sudo ./test_xsk.sh -i IFACE
-#
 # Run test suite in a specific mode only [skb,drv,zc]
 #   sudo ./test_xsk.sh -m MODE
 #
@@ -88,14 +85,11 @@
 
 . xsk_prereqs.sh
 
-ETH=""
-
-while getopts "vi:dm:lt:h" flag
+while getopts "vdm:lt:h" flag
 do
 	case "${flag}" in
 		v) verbose=1;;
 		d) debug=1;;
-		i) ETH=${OPTARG};;
 		m) MODE=${OPTARG};;
 		l) list=1;;
 		t) TEST=${OPTARG};;
@@ -157,21 +151,16 @@ if [[ $help -eq 1 ]]; then
         exit
 fi
 
-if [ ! -z $ETH ]; then
-	VETH0=${ETH}
-	VETH1=${ETH}
-else
-	validate_root_exec
-	validate_veth_support ${VETH0}
-	validate_ip_utility
-	setup_vethPairs
-
-	retval=$?
-	if [ $retval -ne 0 ]; then
-		test_status $retval "${TEST_NAME}"
-		cleanup_exit ${VETH0} ${VETH1}
-		exit $retval
-	fi
+validate_root_exec
+validate_veth_support ${VETH0}
+validate_ip_utility
+setup_vethPairs
+
+retval=$?
+if [ $retval -ne 0 ]; then
+	test_status $retval "${TEST_NAME}"
+	cleanup_exit ${VETH0} ${VETH1}
+	exit $retval
 fi
 
 
@@ -203,11 +192,7 @@ fi
 
 exec_xskxceiver
 
-if [ -z $ETH ]; then
-	cleanup_exit ${VETH0} ${VETH1}
-else
-	cleanup_iface ${ETH} ${MTU}
-fi
+cleanup_exit ${VETH0} ${VETH1}
 
 if [[ $list -eq 1 ]]; then
     exit
@@ -216,18 +201,12 @@ fi
 TEST_NAME="XSK_SELFTESTS_${VETH0}_BUSY_POLL"
 busy_poll=1
 
-if [ -z $ETH ]; then
-	setup_vethPairs
-fi
+setup_vethPairs
 exec_xskxceiver
 
 ## END TESTS
 
-if [ -z $ETH ]; then
-	cleanup_exit ${VETH0} ${VETH1}
-else
-	cleanup_iface ${ETH} ${MTU}
-fi
+cleanup_exit ${VETH0} ${VETH1}
 
 failures=0
 echo -e "\nSummary:"
diff --git a/tools/testing/selftests/bpf/xsk_prereqs.sh b/tools/testing/selftests/bpf/xsk_prereqs.sh
index 47c7b8064f38..ebff0f95d14b 100755
--- a/tools/testing/selftests/bpf/xsk_prereqs.sh
+++ b/tools/testing/selftests/bpf/xsk_prereqs.sh
@@ -53,13 +53,6 @@ test_exit()
 	exit 1
 }
 
-cleanup_iface()
-{
-	ip link set $1 mtu $2
-	ip link set $1 xdp off
-	ip link set $1 xdpgeneric off
-}
-
 clear_configs()
 {
 	[ $(ip link show $1 &>/dev/null; echo $?;) == 0 ] &&
diff --git a/tools/testing/selftests/bpf/xskxceiver.c b/tools/testing/selftests/bpf/xskxceiver.c
index 7dad8556a722..300cb354f421 100644
--- a/tools/testing/selftests/bpf/xskxceiver.c
+++ b/tools/testing/selftests/bpf/xskxceiver.c
@@ -341,7 +341,6 @@ int main(int argc, char **argv)
 	u32 i, j, failed_tests = 0, nb_tests;
 	int modes = TEST_MODE_SKB + 1;
 	struct test_spec test;
-	bool shared_netdev;
 	int ret;
 
 	/* Use libbpf 1.0 API mode */
@@ -388,10 +387,6 @@ int main(int argc, char **argv)
 		ksft_exit_xfail();
 	}
 
-	shared_netdev = (ifobj_tx->ifindex == ifobj_rx->ifindex);
-	ifobj_tx->shared_umem = shared_netdev;
-	ifobj_rx->shared_umem = shared_netdev;
-
 	if (!validate_interface(ifobj_tx) || !validate_interface(ifobj_rx))
 		print_usage(argv);
 
@@ -402,11 +397,8 @@ int main(int argc, char **argv)
 	}
 
 	ret = get_hw_ring_size(ifobj_tx->ifname, &ifobj_tx->ring);
-	if (!ret) {
+	if (!ret)
 		ifobj_tx->hw_ring_size_supp = true;
-		ifobj_tx->set_ring.default_tx = ifobj_tx->ring.tx_pending;
-		ifobj_tx->set_ring.default_rx = ifobj_tx->ring.rx_pending;
-	}
 
 	if (init_iface(ifobj_rx, worker_testapp_validate_rx) ||
 	    init_iface(ifobj_tx, worker_testapp_validate_tx)) {
@@ -462,9 +454,6 @@ int main(int argc, char **argv)
 		}
 	}
 
-	if (ifobj_tx->hw_ring_size_supp)
-		hw_ring_size_reset(ifobj_tx);
-
 	pkt_stream_delete(tx_pkt_stream_default);
 	pkt_stream_delete(rx_pkt_stream_default);
 	xsk_unload_xdp_programs(ifobj_tx);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 04/15] selftests/bpf: drop the test_progs AF_XDP wrapper
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (2 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 05/15] selftests: net: add a generic rule for BPF skeletons Maciej Fijalkowski
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

prog_tests/xsk.c runs the xskxceiver cases inside test_progs, driving
the TX and RX sockets of a veth pair from two threads. The following
patches turn each endpoint into a process of its own that can run
against a peer on another host. Keeping the wrapper would mean rewiring
it at every step of that split and teaching test_progs to spawn and
synchronize peer processes, so drop it. BPF CI loses the ns_xsk_skb and
ns_xsk_drv entries; the same veth coverage stays with test_xsk.sh,
which moves to selftests/net together with the engine.

ci_skip_tests[] held the cases that the wrapper left out: flaky and
slow ones and those that need hugepages or HW ring size support.
Nothing runs only one of the lists anymore, so fold ci_skip_tests[]
into tests[]. The test IDs do not change.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../selftests/bpf/prog_tests/test_xsk.h       |   3 -
 tools/testing/selftests/bpf/prog_tests/xsk.c  | 160 ------------------
 tools/testing/selftests/bpf/xskxceiver.c      |  10 +-
 3 files changed, 2 insertions(+), 171 deletions(-)
 delete mode 100644 tools/testing/selftests/bpf/prog_tests/xsk.c

diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.h b/tools/testing/selftests/bpf/prog_tests/test_xsk.h
index 33331fbee6f5..b3007923e8ac 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.h
+++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.h
@@ -285,9 +285,6 @@ static const struct test_spec tests[] = {
 	{.name = "TOO_MANY_FRAGS", .test_func = testapp_too_many_frags},
 	{.name = "XDP_ADJUST_TAIL_SHRINK", .test_func = testapp_adjust_tail_shrink},
 	{.name = "TX_QUEUE_CONSUMER", .test_func = testapp_tx_queue_consumer},
-	};
-
-static const struct test_spec ci_skip_tests[] = {
 	/* Flaky tests */
 	{.name = "XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF", .test_func = testapp_adjust_tail_shrink_mb},
 	{.name = "XDP_ADJUST_TAIL_GROW", .test_func = testapp_adjust_tail_grow},
diff --git a/tools/testing/selftests/bpf/prog_tests/xsk.c b/tools/testing/selftests/bpf/prog_tests/xsk.c
deleted file mode 100644
index af67e2605818..000000000000
--- a/tools/testing/selftests/bpf/prog_tests/xsk.c
+++ /dev/null
@@ -1,160 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0
-#include <net/if.h>
-#include <stdarg.h>
-
-#include "network_helpers.h"
-#include "test_progs.h"
-#include "test_xsk.h"
-#include "xsk_xdp_progs.skel.h"
-
-#define VETH_RX "veth0"
-#define VETH_TX "veth1"
-#define MTU	1500
-
-int setup_veth(bool busy_poll)
-{
-	SYS(fail,
-	"ip link add %s numtxqueues 4 numrxqueues 4 type veth peer name %s numtxqueues 4 numrxqueues 4",
-	VETH_RX, VETH_TX);
-	SYS(fail, "sysctl -wq net.ipv6.conf.%s.disable_ipv6=1", VETH_RX);
-	SYS(fail, "sysctl -wq net.ipv6.conf.%s.disable_ipv6=1", VETH_TX);
-
-	if (busy_poll) {
-		SYS(fail, "echo 2 > /sys/class/net/%s/napi_defer_hard_irqs", VETH_RX);
-		SYS(fail, "echo 200000 > /sys/class/net/%s/gro_flush_timeout", VETH_RX);
-		SYS(fail, "echo 2 > /sys/class/net/%s/napi_defer_hard_irqs", VETH_TX);
-		SYS(fail, "echo 200000 > /sys/class/net/%s/gro_flush_timeout", VETH_TX);
-	}
-
-	SYS(fail, "ip link set %s mtu %d", VETH_RX, MTU);
-	SYS(fail, "ip link set %s mtu %d", VETH_TX, MTU);
-	SYS(fail, "ip link set %s up", VETH_RX);
-	SYS(fail, "ip link set %s up", VETH_TX);
-
-	return 0;
-
-fail:
-	return -1;
-}
-
-void delete_veth(void)
-{
-	SYS_NOFAIL("ip link del %s", VETH_RX);
-	SYS_NOFAIL("ip link del %s", VETH_TX);
-}
-
-int configure_ifobj(struct ifobject *tx, struct ifobject *rx)
-{
-	rx->ifindex = if_nametoindex(VETH_RX);
-	if (!ASSERT_OK_FD(rx->ifindex, "get RX ifindex"))
-		return -1;
-
-	tx->ifindex = if_nametoindex(VETH_TX);
-	if (!ASSERT_OK_FD(tx->ifindex, "get TX ifindex"))
-		return -1;
-
-	return 0;
-}
-
-static void test_xsk(const struct test_spec *test_to_run, enum test_mode mode)
-{
-	u32 max_frags, umem_tailroom, cache_line_size;
-	struct ifobject *ifobj_tx, *ifobj_rx;
-	struct test_spec test;
-	int ret;
-
-	ifobj_tx = ifobject_create();
-	if (!ASSERT_OK_PTR(ifobj_tx, "create ifobj_tx"))
-		return;
-
-	ifobj_rx = ifobject_create();
-	if (!ASSERT_OK_PTR(ifobj_rx, "create ifobj_rx"))
-		goto delete_tx;
-
-	if (!ASSERT_OK(configure_ifobj(ifobj_tx, ifobj_rx), "conigure ifobj"))
-		goto delete_rx;
-
-	ret = get_hw_ring_size(ifobj_tx->ifname, &ifobj_tx->ring);
-	if (!ret)
-		ifobj_tx->hw_ring_size_supp = true;
-
-	cache_line_size = read_procfs_val(SMP_CACHE_BYTES_PATH);
-	if (!cache_line_size)
-		cache_line_size = 64;
-
-	max_frags = read_procfs_val(MAX_SKB_FRAGS_PATH);
-	if (!max_frags)
-		max_frags = 17;
-
-	ifobj_tx->max_skb_frags = max_frags;
-	ifobj_rx->max_skb_frags = max_frags;
-
-	/* 48 bytes is a part of skb_shared_info w/o frags array;
-	 * 16 bytes is sizeof(skb_frag_t)
-	 */
-	umem_tailroom = ALIGN(48 + (max_frags * 16), cache_line_size);
-	ifobj_tx->umem_tailroom = umem_tailroom;
-	ifobj_rx->umem_tailroom = umem_tailroom;
-
-	if (!ASSERT_OK(init_iface(ifobj_rx, worker_testapp_validate_rx), "init RX"))
-		goto delete_rx;
-	if (!ASSERT_OK(init_iface(ifobj_tx, worker_testapp_validate_tx), "init TX"))
-		goto delete_rx;
-
-	test_init(&test, ifobj_tx, ifobj_rx, 0, &tests[0]);
-
-	test.tx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
-	if (!ASSERT_OK_PTR(test.tx_pkt_stream_default, "TX pkt generation"))
-		goto delete_rx;
-	test.rx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
-	if (!ASSERT_OK_PTR(test.rx_pkt_stream_default, "RX pkt generation"))
-		goto delete_rx;
-
-
-	test_init(&test, ifobj_tx, ifobj_rx, mode, test_to_run);
-	ret = test.test_func(&test);
-	if (ret != TEST_SKIP)
-		ASSERT_OK(ret, "Run test");
-	pkt_stream_restore_default(&test);
-
-	pkt_stream_delete(test.tx_pkt_stream_default);
-	pkt_stream_delete(test.rx_pkt_stream_default);
-	xsk_xdp_progs__destroy(ifobj_tx->xdp_progs);
-	xsk_xdp_progs__destroy(ifobj_rx->xdp_progs);
-
-delete_rx:
-	ifobject_delete(ifobj_rx);
-delete_tx:
-	ifobject_delete(ifobj_tx);
-}
-
-void test_ns_xsk_skb(void)
-{
-	int i;
-
-	if (!ASSERT_OK(setup_veth(false), "setup veth"))
-		return;
-
-	for (i = 0; i < ARRAY_SIZE(tests); i++) {
-		if (test__start_subtest(tests[i].name))
-			test_xsk(&tests[i], TEST_MODE_SKB);
-	}
-
-	delete_veth();
-}
-
-void test_ns_xsk_drv(void)
-{
-	int i;
-
-	if (!ASSERT_OK(setup_veth(false), "setup veth"))
-		return;
-
-	for (i = 0; i < ARRAY_SIZE(tests); i++) {
-		if (test__start_subtest(tests[i].name))
-			test_xsk(&tests[i], TEST_MODE_DRV);
-	}
-
-	delete_veth();
-}
-
diff --git a/tools/testing/selftests/bpf/xskxceiver.c b/tools/testing/selftests/bpf/xskxceiver.c
index 300cb354f421..102283480a9a 100644
--- a/tools/testing/selftests/bpf/xskxceiver.c
+++ b/tools/testing/selftests/bpf/xskxceiver.c
@@ -327,14 +327,12 @@ static void print_tests(void)
 	printf("Tests:\n");
 	for (i = 0; i < ARRAY_SIZE(tests); i++)
 		printf("%u: %s\n", i, tests[i].name);
-	for (i = ARRAY_SIZE(tests); i < ARRAY_SIZE(tests) + ARRAY_SIZE(ci_skip_tests); i++)
-		printf("%u: %s\n", i, ci_skip_tests[i - ARRAY_SIZE(tests)].name);
 }
 
 int main(int argc, char **argv)
 {
-	const size_t total_tests = ARRAY_SIZE(tests) + ARRAY_SIZE(ci_skip_tests);
 	u32 cache_line_size, max_frags, umem_tailroom;
+	const size_t total_tests = ARRAY_SIZE(tests);
 	struct pkt_stream *rx_pkt_stream_default;
 	struct pkt_stream *tx_pkt_stream_default;
 	struct ifobject *ifobj_tx, *ifobj_rx;
@@ -441,11 +439,7 @@ int main(int argc, char **argv)
 			if (opt_run_test != RUN_ALL_TESTS && j != opt_run_test)
 				continue;
 
-			if (j < ARRAY_SIZE(tests))
-				test_init(&test, ifobj_tx, ifobj_rx, i, &tests[j]);
-			else
-				test_init(&test, ifobj_tx, ifobj_rx, i,
-					  &ci_skip_tests[j - ARRAY_SIZE(tests)]);
+			test_init(&test, ifobj_tx, ifobj_rx, i, &tests[j]);
 			run_pkt_test(&test);
 			usleep(USLEEP_MAX);
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 05/15] selftests: net: add a generic rule for BPF skeletons
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (3 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 04/15] selftests/bpf: drop the test_progs AF_XDP wrapper Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 06/15] selftests: xsk: move the AF_XDP test suite to selftests/net Maciej Fijalkowski
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

net/bpf.mk builds the BPF programs of the networking selftests, but a
test that loads its program through a bpftool-generated skeleton has to
bring its own rule for the header. Add a pattern rule that generates
$(OUTPUT)/<name>.skel.h from $(OUTPUT)/<name>.bpf.o with $(BPFTOOL),
which defaults to the bpftool in PATH. A failing bpftool leaves no
partial header behind for a later build to take as up to date.

xskxceiver, which moves to net/lib in the next patch, is the first user.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 tools/testing/selftests/net/bpf.mk | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/tools/testing/selftests/net/bpf.mk b/tools/testing/selftests/net/bpf.mk
index a4f6755dd894..0eca0c5de5b5 100644
--- a/tools/testing/selftests/net/bpf.mk
+++ b/tools/testing/selftests/net/bpf.mk
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 # Rules to generate bpf objs
 CLANG ?= clang
+BPFTOOL ?= bpftool
 SCRATCH_DIR := $(OUTPUT)/tools
 BUILD_DIR := $(SCRATCH_DIR)/build
 BPFDIR := $(top_srcdir)/tools/lib/bpf
@@ -42,6 +43,11 @@ $(BPF_PROG_OBJS): $(OUTPUT)/%.o : %.c $(BPFOBJ) | $(MAKE_DIRS)
 	$(Q)$(CLANG) -O2 -g --target=bpf $(CCINCLUDE) $(CLANG_SYS_INCLUDES) \
 	-c $< -o $@
 
+$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o
+	$(call msg,GEN-SKEL,,$@)
+	$(Q)$(BPFTOOL) gen skeleton $< name $(notdir $*) > $@ || \
+		{ rm -f $@; exit 1; }
+
 $(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile)		       \
 	   $(APIDIR)/linux/bpf.h					       \
 	   | $(BUILD_DIR)/libbpf
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 06/15] selftests: xsk: move the AF_XDP test suite to selftests/net
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (4 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 05/15] selftests: net: add a generic rule for BPF skeletons Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 07/15] selftests: xsk: collect interface capabilities in struct xsk_caps Maciej Fijalkowski
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Move the xskxceiver engine, the AF_XDP userspace library, the test
definitions and the XDP program to net/lib/xsk, and test_xsk.sh with
its prerequisites script to selftests/net. A later patch drives the
same engine against real NICs from drivers/net/hw, and net/lib is where
both suites take shared helpers from.

net/lib builds xskxceiver with its XDP program and skeleton, using the
bpftool in PATH, and skips it with a warning when bpftool is missing or
does not run; test_xsk.sh then skips too. xskxceiver links -lpthread,
which glibc older than 2.34 needs for the threads of test_xsk.c.

Like the rest of net/lib, xskxceiver builds against the installed uapi
headers, so include linux/kernel.h for min_t() directly rather than via
the tools copy of linux/netlink.h. tools/include goes ahead of CFLAGS,
so that O= builds take linux/kernel.h and linux/types.h from it rather
than from a source tree usr/include.

Move the ethtool ring helper from network_helpers into xsk.c and
point the kselftest.h include of test_xsk.h at the net/lib include
path. The XDP program moves unchanged; its Ethernet header bound check
compares pointers of distinct types, so net/lib builds it with
-Wno-compare-distinct-pointer-types, as the bpf selftests do.
xdp_hw_metadata and the xdp_metadata test keep using xsk.c and xsk.h
from their new location. Update the AF_XDP entry in MAINTAINERS.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 Documentation/networking/af_xdp.rst           |  6 ++--
 MAINTAINERS                                   |  3 +-
 tools/testing/selftests/bpf/.gitignore        |  1 -
 tools/testing/selftests/bpf/Makefile          | 29 +++++++--------
 tools/testing/selftests/bpf/network_helpers.c | 24 -------------
 tools/testing/selftests/bpf/network_helpers.h |  1 -
 tools/testing/selftests/net/Makefile          |  2 ++
 tools/testing/selftests/net/config            |  1 +
 tools/testing/selftests/net/lib/.gitignore    |  2 ++
 tools/testing/selftests/net/lib/Makefile      | 35 +++++++++++++++++++
 .../prog_tests => net/lib/xsk}/test_xsk.c     |  2 +-
 .../prog_tests => net/lib/xsk}/test_xsk.h     |  2 +-
 .../selftests/{bpf => net/lib/xsk}/xsk.c      | 25 ++++++++++++-
 .../selftests/{bpf => net/lib/xsk}/xsk.h      |  3 ++
 .../{bpf => net/lib/xsk}/xsk_xdp_common.h     |  0
 .../lib/xsk/xsk_xdp_progs.bpf.c}              |  0
 .../{bpf => net/lib/xsk}/xskxceiver.c         |  7 ++--
 .../{bpf => net/lib/xsk}/xskxceiver.h         |  0
 .../selftests/{bpf => net}/test_xsk.sh        |  5 +++
 .../selftests/{bpf => net}/xsk_prereqs.sh     |  2 +-
 20 files changed, 94 insertions(+), 56 deletions(-)
 rename tools/testing/selftests/{bpf/prog_tests => net/lib/xsk}/test_xsk.c (99%)
 rename tools/testing/selftests/{bpf/prog_tests => net/lib/xsk}/test_xsk.h (99%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk.c (97%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk.h (98%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xsk_xdp_common.h (100%)
 rename tools/testing/selftests/{bpf/progs/xsk_xdp_progs.c => net/lib/xsk/xsk_xdp_progs.bpf.c} (100%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xskxceiver.c (99%)
 rename tools/testing/selftests/{bpf => net/lib/xsk}/xskxceiver.h (100%)
 rename tools/testing/selftests/{bpf => net}/test_xsk.sh (98%)
 rename tools/testing/selftests/{bpf => net}/xsk_prereqs.sh (98%)

diff --git a/Documentation/networking/af_xdp.rst b/Documentation/networking/af_xdp.rst
index cc3f0d16b28f..538e71adb759 100644
--- a/Documentation/networking/af_xdp.rst
+++ b/Documentation/networking/af_xdp.rst
@@ -211,7 +211,7 @@ Libbpf
 
 Libbpf is a helper library for eBPF and XDP that makes using these
 technologies a lot simpler. It also contains specific helper functions
-in tools/testing/selftests/bpf/xsk.h for facilitating the use of
+in tools/testing/selftests/net/lib/xsk/xsk.h for facilitating the use of
 AF_XDP. It contains two types of functions: those that can be used to
 make the setup of AF_XDP socket easier and ones that can be used in the
 data plane to access the rings safely and quickly.
@@ -751,7 +751,7 @@ return values:
      returned number signifies the max number of frags supported.
 
 For an example on how these are used through libbpf, please take a
-look at tools/testing/selftests/bpf/xskxceiver.c.
+look at tools/testing/selftests/net/lib/xsk/xskxceiver.c.
 
 Multi-Buffer Support for Zero-Copy Drivers
 ------------------------------------------
@@ -785,7 +785,7 @@ can be displayed with "-h", as usual.
 This sample application uses libbpf to make the setup and usage of
 AF_XDP simpler. If you want to know how the raw uapi of AF_XDP is
 really used to make something more advanced, take a look at the libbpf
-code in tools/testing/selftests/bpf/xsk.[ch].
+code in tools/testing/selftests/net/lib/xsk/xsk.[ch].
 
 FAQ
 =======
diff --git a/MAINTAINERS b/MAINTAINERS
index 509b35b5a0e7..1fc9eaedd744 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29783,7 +29783,8 @@ F:	include/net/xsk_buff_pool.h
 F:	include/uapi/linux/if_xdp.h
 F:	include/uapi/linux/xdp_diag.h
 F:	net/xdp/
-F:	tools/testing/selftests/bpf/*xsk*
+F:	tools/testing/selftests/net/*xsk*
+F:	tools/testing/selftests/net/lib/xsk/
 
 XEN BLOCK SUBSYSTEM
 M:	Roger Pau Monné <roger@xenproject.org>
diff --git a/tools/testing/selftests/bpf/.gitignore b/tools/testing/selftests/bpf/.gitignore
index 986a6389186b..3c3183ea3c83 100644
--- a/tools/testing/selftests/bpf/.gitignore
+++ b/tools/testing/selftests/bpf/.gitignore
@@ -37,7 +37,6 @@ test_cpp
 /uprobe_multi
 *.ko
 *.tmp
-xskxceiver
 xdp_redirect_multi
 xdp_synproxy
 xdp_hw_metadata
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index 93c707116fad..a2d357781c0f 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -62,7 +62,8 @@ COMMON_CFLAGS = -g $(OPT_FLAGS) -rdynamic -std=gnu11				\
 	  $(GENFLAGS) $(SAN_CFLAGS) $(LIBELF_CFLAGS)			\
 	  -I$(CURDIR) -I$(INCLUDE_DIR) -I$(GENDIR) -I$(LIBDIR)		\
 	  -I$(TOOLSINCDIR) -I$(TOOLSARCHINCDIR) -I$(APIDIR) -I$(OUTPUT)	\
-	  -I$(CURDIR)/libarena/include
+	  -I$(CURDIR)/libarena/include					\
+	  -I$(CURDIR)/../net/lib/xsk
 LDFLAGS += $(SAN_LDFLAGS)
 LDLIBS += $(LIBELF_LIBS) -lz -lrt -lpthread
 
@@ -118,14 +119,13 @@ TEST_GEN_PROGS += test_progs-cpuv4
 TEST_INST_SUBDIRS += cpuv4
 endif
 
-TEST_FILES = xsk_prereqs.sh $(wildcard progs/btf_dump_test_case_*.c)
+TEST_FILES = $(wildcard progs/btf_dump_test_case_*.c)
 
 # Order correspond to 'make run_tests' order
 TEST_PROGS := test_kmod.sh \
 	test_lirc_mode2.sh \
 	test_bpftool_build.sh \
 	test_doc_build.sh \
-	test_xsk.sh \
 	test_xdp_features.sh
 
 TEST_PROGS_EXTENDED := \
@@ -144,8 +144,7 @@ TEST_GEN_PROGS_EXTENDED = \
 	veristat \
 	xdp_features \
 	xdp_hw_metadata \
-	xdp_synproxy \
-	xskxceiver
+	xdp_synproxy
 
 TEST_GEN_FILES += $(TEST_KMODS) liburandom_read.so urandom_read sign-file uprobe_multi
 
@@ -542,7 +541,6 @@ linked_maps.skel.h-deps := linked_maps1.bpf.o linked_maps2.bpf.o
 test_subskeleton.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o test_subskeleton.bpf.o
 test_subskeleton_lib.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o
 test_usdt.skel.h-deps := test_usdt.bpf.o test_usdt_multispec.bpf.o
-xsk_xdp_progs.skel.h-deps := xsk_xdp_progs.bpf.o
 xdp_hw_metadata.skel.h-deps := xdp_hw_metadata.bpf.o
 xdp_features.skel.h-deps := xdp_features.bpf.o
 tracing_multi.skel.h-deps := tracing_multi_attach.bpf.o tracing_multi_check.bpf.o
@@ -769,6 +767,8 @@ $(TRUNNER_EXTRA_OBJS): $(TRUNNER_OUTPUT)/%.o:				\
 	$$(call msg,EXT-OBJ,$(TRUNNER_BINARY),$$@)
 	$(Q)$$(CC) $$(CFLAGS) -c $$< $$(LDLIBS) -o $$@
 
+$(TRUNNER_OUTPUT)/xsk.o: ../net/lib/xsk/xsk.h
+
 $(TRUNNER_LIB_OBJS): $(TRUNNER_OUTPUT)/%.o:$(TOOLSDIR)/lib/%.c
 	$$(call msg,LIB-OBJ,$(TRUNNER_BINARY),$$@)
 	$(Q)$$(CC) $$(CFLAGS) -c $$< $$(LDLIBS) -o $$@
@@ -845,6 +845,10 @@ $(LIBARENA_ASAN_SKEL): $(INCLUDE_DIR)/vmlinux.h $(BPFOBJ) $(LIBARENA_BPF_DEPS)
 	+$(MAKE) -C libarena libarena_asan.skel.h $(LIBARENA_MAKE_ARGS)
 endif
 
+# xsk.c lives in ../net/lib/xsk so it can be shared with selftests/net;
+# let make find it there instead of keeping a local copy.
+vpath %.c ../net/lib/xsk
+
 # Define test_progs test runner.
 TRUNNER_TESTS_DIR := prog_tests
 TRUNNER_BPF_PROGS_DIR := progs
@@ -931,18 +935,9 @@ $(OUTPUT)/test_verifier: test_verifier.c verifier/tests.h $(BPFOBJ) | $(OUTPUT)
 	$(call msg,BINARY,,$@)
 	$(Q)$(CC) $(CFLAGS) $(filter %.a %.o %.c,$^) $(LDLIBS) -o $@
 
-# Keep xskxceiver independent from test_progs object dependencies.
-$(OUTPUT)/xskxceiver: xskxceiver.c xsk.c network_helpers.c \
-			   $(TOOLSDIR)/lib/find_bit.c prog_tests/test_xsk.c \
-			   xskxceiver.h xsk.h network_helpers.h \
-			   prog_tests/test_xsk.h test_progs.h bpf_util.h \
-			   $(OUTPUT)/xsk_xdp_progs.skel.h $(BPFOBJ) | $(OUTPUT)
-	$(call msg,BINARY,,$@)
-	$(Q)$(CC) $(CFLAGS) $(filter %.a %.o %.c,$^) $(LDLIBS) -o $@
-
-$(OUTPUT)/xdp_hw_metadata: xdp_hw_metadata.c xsk.c network_helpers.c \
+$(OUTPUT)/xdp_hw_metadata: xdp_hw_metadata.c ../net/lib/xsk/xsk.c network_helpers.c \
 			   $(TOOLSDIR)/lib/find_bit.c xdp_metadata.h \
-			   xsk.h network_helpers.h test_progs.h bpf_util.h \
+			   ../net/lib/xsk/xsk.h network_helpers.h test_progs.h bpf_util.h \
 			   $(OUTPUT)/xdp_hw_metadata.skel.h $(BPFOBJ) | $(OUTPUT)
 	$(call msg,BINARY,,$@)
 	$(Q)$(CC) $(CFLAGS) $(filter %.a %.o %.c,$^) $(LDLIBS) -o $@
diff --git a/tools/testing/selftests/bpf/network_helpers.c b/tools/testing/selftests/bpf/network_helpers.c
index 888f03609175..d72d7022cc69 100644
--- a/tools/testing/selftests/bpf/network_helpers.c
+++ b/tools/testing/selftests/bpf/network_helpers.c
@@ -622,30 +622,6 @@ int get_socket_local_port(int sock_fd)
 	return -1;
 }
 
-int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param)
-{
-	struct ifreq ifr = {0};
-	int sockfd, err;
-
-	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
-	if (sockfd < 0)
-		return -errno;
-
-	memcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
-
-	ring_param->cmd = ETHTOOL_GRINGPARAM;
-	ifr.ifr_data = (char *)ring_param;
-
-	if (ioctl(sockfd, SIOCETHTOOL, &ifr) < 0) {
-		err = errno;
-		close(sockfd);
-		return -err;
-	}
-
-	close(sockfd);
-	return 0;
-}
-
 struct send_recv_arg {
 	int		fd;
 	uint32_t	bytes;
diff --git a/tools/testing/selftests/bpf/network_helpers.h b/tools/testing/selftests/bpf/network_helpers.h
index c7c52597c33f..e7333b769150 100644
--- a/tools/testing/selftests/bpf/network_helpers.h
+++ b/tools/testing/selftests/bpf/network_helpers.h
@@ -89,7 +89,6 @@ int make_sockaddr(int family, const char *addr_str, __u16 port,
 		  struct sockaddr_storage *addr, socklen_t *len);
 char *ping_command(int family);
 int get_socket_local_port(int sock_fd);
-int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
 
 int open_tuntap(const char *dev_name, bool need_mac);
 
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 54beea2e348c..aa31b6216dd1 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -118,6 +118,7 @@ TEST_PROGS := \
 	test_vxlan_under_vrf.sh \
 	test_vxlan_vnifilter_notify.sh \
 	test_vxlan_vnifiltering.sh \
+	test_xsk.sh \
 	tfo_passive.sh \
 	traceroute.sh \
 	txtimestamp.sh \
@@ -209,6 +210,7 @@ TEST_FILES := \
 	in_netns.sh \
 	lib.sh \
 	settings \
+	xsk_prereqs.sh \
 # end of TEST_FILES
 
 # YNL files, must be before "include ..lib.mk"
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index d355cf980597..bdaa052eb052 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -137,5 +137,6 @@ CONFIG_USER_NS=y
 CONFIG_VETH=y
 CONFIG_VLAN_8021Q=y
 CONFIG_VXLAN=m
+CONFIG_XDP_SOCKETS=y
 CONFIG_XFRM_INTERFACE=m
 CONFIG_XFRM_USER=m
diff --git a/tools/testing/selftests/net/lib/.gitignore b/tools/testing/selftests/net/lib/.gitignore
index 6cd2b762af5d..1106fef3041f 100644
--- a/tools/testing/selftests/net/lib/.gitignore
+++ b/tools/testing/selftests/net/lib/.gitignore
@@ -2,3 +2,5 @@
 csum
 gro
 xdp_helper
+xsk_xdp_progs.skel.h
+xskxceiver
diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile
index ff83603397d0..c26088da0489 100644
--- a/tools/testing/selftests/net/lib/Makefile
+++ b/tools/testing/selftests/net/lib/Makefile
@@ -5,6 +5,15 @@ CFLAGS += -I../../../../../usr/include/ $(KHDR_INCLUDES)
 # Additional include paths needed by kselftest.h
 CFLAGS += -I../../
 
+# xskxceiver needs a working bpftool to generate its skeleton
+BPFTOOL ?= bpftool
+HAS_BPFTOOL := $(shell $(BPFTOOL) version >/dev/null 2>&1 && echo y)
+ifeq ($(HAS_BPFTOOL),y)
+COND_GEN_FILES += xskxceiver
+else
+$(warning excluding xskxceiver, bpftool not installed or not working)
+endif
+
 TEST_FILES := \
 	../../../../net/ynl \
 	../../../../../Documentation/netlink/specs \
@@ -13,6 +22,7 @@ TEST_FILES := \
 
 TEST_GEN_FILES := \
 	$(patsubst %.c,%.o,$(wildcard *.bpf.c)) \
+	$(COND_GEN_FILES) \
 	csum \
 	gro \
 	xdp_helper \
@@ -23,3 +33,28 @@ TEST_INCLUDES := $(wildcard py/*.py sh/*.sh)
 include ../../lib.mk
 
 include ../bpf.mk
+
+# AF_XDP test engine used by net/test_xsk.sh.
+XSK_DIR := xsk
+XSK_BPF_OBJ := $(OUTPUT)/xsk_xdp_progs.bpf.o
+XSK_SKEL := $(OUTPUT)/xsk_xdp_progs.skel.h
+XSK_SOURCES := $(addprefix $(XSK_DIR)/,xskxceiver.c xsk.c test_xsk.c) \
+	$(top_srcdir)/tools/lib/find_bit.c
+XSK_HEADERS := $(wildcard $(XSK_DIR)/*.h)
+
+PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config
+XSK_LDLIBS := $(shell $(PKG_CONFIG) libelf --libs 2>/dev/null || echo -lelf) -lz
+XSK_LDLIBS += -lpthread
+
+$(XSK_BPF_OBJ): $(XSK_DIR)/xsk_xdp_progs.bpf.c $(XSK_DIR)/xsk_xdp_common.h $(BPFOBJ)
+	$(call msg,BPF_PROG,,$@)
+	$(Q)$(CLANG) -O2 -g --target=bpf $(CCINCLUDE) $(CLANG_SYS_INCLUDES) \
+		-Wno-compare-distinct-pointer-types -c $< -o $@
+
+$(OUTPUT)/xskxceiver: $(XSK_SOURCES) $(XSK_HEADERS) $(XSK_SKEL) $(BPFOBJ)
+	$(call msg,CC,,$@)
+	$(Q)$(CC) -I$(top_srcdir)/tools/include $(CFLAGS) -I$(OUTPUT) \
+		-I$(SCRATCH_DIR)/include \
+		$(XSK_SOURCES) $(BPFOBJ) $(LDLIBS) $(XSK_LDLIBS) -o $@
+
+EXTRA_CLEAN += $(XSK_BPF_OBJ) $(XSK_SKEL)
diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
similarity index 99%
rename from tools/testing/selftests/bpf/prog_tests/test_xsk.c
rename to tools/testing/selftests/net/lib/xsk/test_xsk.c
index b9126fe66e1a..63267d886b17 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: GPL-2.0
+#include <arpa/inet.h>
 #include <bpf/bpf.h>
 #include <errno.h>
 #include <linux/bitmap.h>
@@ -13,7 +14,6 @@
 #include <sys/time.h>
 #include <unistd.h>
 
-#include "network_helpers.h"
 #include "test_xsk.h"
 #include "xsk_xdp_common.h"
 #include "xsk_xdp_progs.skel.h"
diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
similarity index 99%
rename from tools/testing/selftests/bpf/prog_tests/test_xsk.h
rename to tools/testing/selftests/net/lib/xsk/test_xsk.h
index b3007923e8ac..410702f89dc9 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -5,7 +5,7 @@
 #include <linux/ethtool.h>
 #include <linux/if_xdp.h>
 
-#include "../kselftest.h"
+#include "kselftest.h"
 #include "xsk.h"
 
 #ifndef SO_PREFER_BUSY_POLL
diff --git a/tools/testing/selftests/bpf/xsk.c b/tools/testing/selftests/net/lib/xsk/xsk.c
similarity index 97%
rename from tools/testing/selftests/bpf/xsk.c
rename to tools/testing/selftests/net/lib/xsk/xsk.c
index 25d568abf0f2..660540efc46e 100644
--- a/tools/testing/selftests/bpf/xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/xsk.c
@@ -35,7 +35,6 @@
 #include <bpf/bpf.h>
 #include <bpf/libbpf.h>
 #include "xsk.h"
-#include "bpf_util.h"
 
 #ifndef SOL_XDP
  #define SOL_XDP 283
@@ -779,3 +778,27 @@ void xsk_socket__delete(struct xsk_socket *xsk)
 		close(xsk->fd);
 	free(xsk);
 }
+
+int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param)
+{
+	struct ifreq ifr = {0};
+	int sockfd, err;
+
+	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (sockfd < 0)
+		return -errno;
+
+	memcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
+
+	ring_param->cmd = ETHTOOL_GRINGPARAM;
+	ifr.ifr_data = (char *)ring_param;
+
+	if (ioctl(sockfd, SIOCETHTOOL, &ifr) < 0) {
+		err = errno;
+		close(sockfd);
+		return -err;
+	}
+
+	close(sockfd);
+	return 0;
+}
diff --git a/tools/testing/selftests/bpf/xsk.h b/tools/testing/selftests/net/lib/xsk/xsk.h
similarity index 98%
rename from tools/testing/selftests/bpf/xsk.h
rename to tools/testing/selftests/net/lib/xsk/xsk.h
index 48729da142c2..94a56dd43f2e 100644
--- a/tools/testing/selftests/bpf/xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/xsk.h
@@ -15,6 +15,7 @@
 #include <stdio.h>
 #include <stdint.h>
 #include <stdbool.h>
+#include <linux/ethtool.h>
 #include <linux/if_xdp.h>
 
 #include <bpf/libbpf.h>
@@ -242,6 +243,8 @@ void xsk_socket__delete(struct xsk_socket *xsk);
 
 int xsk_set_mtu(int ifindex, int mtu);
 
+int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
diff --git a/tools/testing/selftests/bpf/xsk_xdp_common.h b/tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h
similarity index 100%
rename from tools/testing/selftests/bpf/xsk_xdp_common.h
rename to tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h
diff --git a/tools/testing/selftests/bpf/progs/xsk_xdp_progs.c b/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
similarity index 100%
rename from tools/testing/selftests/bpf/progs/xsk_xdp_progs.c
rename to tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
diff --git a/tools/testing/selftests/bpf/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
similarity index 99%
rename from tools/testing/selftests/bpf/xskxceiver.c
rename to tools/testing/selftests/net/lib/xsk/xskxceiver.c
index 102283480a9a..7876ad47ed0f 100644
--- a/tools/testing/selftests/bpf/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -81,6 +81,7 @@
 #include <linux/netdev.h>
 #include <linux/ethtool.h>
 #include <linux/align.h>
+#include <linux/kernel.h>
 #include <arpa/inet.h>
 #include <net/if.h>
 #include <locale.h>
@@ -91,7 +92,7 @@
 #include <sys/mman.h>
 #include <sys/types.h>
 
-#include "prog_tests/test_xsk.h"
+#include "test_xsk.h"
 #include "xsk_xdp_progs.skel.h"
 #include "xsk.h"
 #include "xskxceiver.h"
@@ -100,14 +101,10 @@
 #include "kselftest.h"
 #include "xsk_xdp_common.h"
 
-#include <network_helpers.h>
-
 static bool opt_print_tests;
 static enum test_mode opt_mode = TEST_MODE_ALL;
 static u32 opt_run_test = RUN_ALL_TESTS;
 
-void test__fail(void) { /* for network_helpers.c */ }
-
 static void __exit_with_error(int error, const char *file, const char *func, int line)
 {
 	ksft_test_result_fail("[%s:%s:%i]: ERROR: %d/\"%s\"\n", file, func, line,
diff --git a/tools/testing/selftests/bpf/xskxceiver.h b/tools/testing/selftests/net/lib/xsk/xskxceiver.h
similarity index 100%
rename from tools/testing/selftests/bpf/xskxceiver.h
rename to tools/testing/selftests/net/lib/xsk/xskxceiver.h
diff --git a/tools/testing/selftests/bpf/test_xsk.sh b/tools/testing/selftests/net/test_xsk.sh
similarity index 98%
rename from tools/testing/selftests/bpf/test_xsk.sh
rename to tools/testing/selftests/net/test_xsk.sh
index 69fac65f073c..fbf4551d596a 100755
--- a/tools/testing/selftests/bpf/test_xsk.sh
+++ b/tools/testing/selftests/net/test_xsk.sh
@@ -97,6 +97,11 @@ do
 	esac
 done
 
+if [ ! -x "./${XSKOBJ}" ]; then
+	echo "xskxceiver was not built; skipping"
+	exit $ksft_skip
+fi
+
 TEST_NAME="PREREQUISITES"
 
 URANDOM=/dev/urandom
diff --git a/tools/testing/selftests/bpf/xsk_prereqs.sh b/tools/testing/selftests/net/xsk_prereqs.sh
similarity index 98%
rename from tools/testing/selftests/bpf/xsk_prereqs.sh
rename to tools/testing/selftests/net/xsk_prereqs.sh
index ebff0f95d14b..5e5c8ef3fff2 100755
--- a/tools/testing/selftests/bpf/xsk_prereqs.sh
+++ b/tools/testing/selftests/net/xsk_prereqs.sh
@@ -8,7 +8,7 @@ ksft_xfail=2
 ksft_xpass=3
 ksft_skip=4
 
-XSKOBJ=xskxceiver
+XSKOBJ=lib/xskxceiver
 
 validate_root_exec()
 {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 07/15] selftests: xsk: collect interface capabilities in struct xsk_caps
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (5 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 06/15] selftests: xsk: move the AF_XDP test suite to selftests/net Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 08/15] selftests: xsk: split xskxceiver main() into setup, run and cleanup Maciej Fijalkowski
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

A later patch runs the TX and RX endpoints of a case as separate
processes, possibly on different hosts, each binding only its own
interface. The shared test code still looks at the capabilities of both
ifobjects, e.g. to check both for multi-buffer support or to size the
RX batch from the TX ring limit. As the endpoints do not exchange
capabilities, a process will represent the other side by a shadow
ifobject that mirrors the local ones.

Prepare for that by replacing the per-interface support booleans and
limits in struct ifobject with one struct xsk_caps, so that mirroring
takes a single struct assignment and a capability added later cannot
be left out of the copy. The maximum TX ring size, which comes from the
ethtool ring parameters that a shadow does not mirror, is copied there
too. ifobj_has_cap() and ifobj_set_cap() test and set the flags, and
xsk_get_cap() reads a limit from the TX ifobject, the only one that has
all of them set, as the open-coded reads did.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/test_xsk.c  | 82 +++++++++++--------
 .../testing/selftests/net/lib/xsk/test_xsk.h  | 34 ++++++--
 .../selftests/net/lib/xsk/xskxceiver.c        | 14 ++--
 3 files changed, 82 insertions(+), 48 deletions(-)

diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
index 63267d886b17..edb1feb54c54 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -1743,20 +1743,24 @@ static int __testapp_validate_traffic(struct test_spec *test, struct ifobject *i
 				      struct ifobject *ifobj2)
 {
 	pthread_t t0, t1;
+	u32 mbuf_cap;
 	int err;
 
-	if (test->mtu > MAX_ETH_PKT_SIZE) {
-		if (test->mode == TEST_MODE_ZC && (!ifobj1->multi_buff_zc_supp ||
-						   (ifobj2 && !ifobj2->multi_buff_zc_supp))) {
-			ksft_print_msg("Multi buffer for zero-copy not supported.\n");
-			return TEST_SKIP;
-		}
-		if (test->mode != TEST_MODE_ZC && (!ifobj1->multi_buff_supp ||
-						   (ifobj2 && !ifobj2->multi_buff_supp))) {
-			ksft_print_msg("Multi buffer not supported.\n");
-			return TEST_SKIP;
-		}
+	if (test->mtu <= MAX_ETH_PKT_SIZE)
+		goto skip_mbuf_check;
+
+	mbuf_cap = test->mode == TEST_MODE_ZC ? XSK_CAP_MBUF_ZC :
+						XSK_CAP_MBUF;
+
+	if (!ifobj_has_cap(ifobj1, mbuf_cap) ||
+	    (ifobj2 && !ifobj_has_cap(ifobj2, mbuf_cap))) {
+		ksft_print_msg("Multi buffer%s not supported.\n",
+			       mbuf_cap == XSK_CAP_MBUF_ZC ?
+			       " for zero-copy" : "");
+		return TEST_SKIP;
 	}
+
+skip_mbuf_check:
 	err = test_spec_set_mtu(test, test->mtu);
 	if (err) {
 		ksft_print_msg("Error, could not set mtu.\n");
@@ -1821,8 +1825,10 @@ static int testapp_validate_traffic(struct test_spec *test)
 	struct ifobject *ifobj_rx = test->ifobj_rx;
 	struct ifobject *ifobj_tx = test->ifobj_tx;
 
-	if ((ifobj_rx->xsk->umem->unaligned_mode && !ifobj_rx->unaligned_supp) ||
-	    (ifobj_tx->xsk->umem->unaligned_mode && !ifobj_tx->unaligned_supp)) {
+	if ((ifobj_rx->xsk->umem->unaligned_mode &&
+	     !ifobj_has_cap(ifobj_rx, XSK_CAP_UNALIGNED)) ||
+	    (ifobj_tx->xsk->umem->unaligned_mode &&
+	     !ifobj_has_cap(ifobj_tx, XSK_CAP_UNALIGNED))) {
 		ksft_print_msg("No huge pages present.\n");
 		return TEST_SKIP;
 	}
@@ -1922,7 +1928,7 @@ int testapp_headroom(struct test_spec *test)
 int testapp_stats_rx_dropped(struct test_spec *test)
 {
 	struct xsk_umem_info *umem = test->ifobj_rx->xsk->umem;
-	u32 umem_tr = test->ifobj_tx->umem_tailroom;
+	u32 umem_tr = xsk_get_cap(test, umem_tailroom);
 
 	if (test->mode == TEST_MODE_ZC) {
 		ksft_print_msg("Can not run RX_DROPPED test for ZC mode\n");
@@ -2184,9 +2190,9 @@ int testapp_too_many_frags(struct test_spec *test)
 	int ret = TEST_FAILURE;
 
 	if (test->mode == TEST_MODE_ZC) {
-		max_frags = test->ifobj_tx->xdp_zc_max_segs;
+		max_frags = xsk_get_cap(test, xdp_zc_max_segs);
 	} else {
-		max_frags = test->ifobj_tx->max_skb_frags;
+		max_frags = xsk_get_cap(test, max_skb_frags);
 		max_frags += 1;
 	}
 
@@ -2246,7 +2252,6 @@ static int xsk_load_xdp_programs(struct ifobject *ifobj)
 	return 0;
 }
 
-/* Simple test */
 static bool hugepages_present(void)
 {
 	size_t mmap_sz = 2 * DEFAULT_UMEM_BUFFERS * XSK_UMEM__DEFAULT_FRAME_SIZE;
@@ -2262,21 +2267,13 @@ static bool hugepages_present(void)
 	return true;
 }
 
-int init_iface(struct ifobject *ifobj, thread_func_t func_ptr)
+static int detect_ifobj_caps(struct ifobject *ifobj)
 {
 	LIBBPF_OPTS(bpf_xdp_query_opts, query_opts);
 	int err;
 
-	ifobj->func_ptr = func_ptr;
-
-	err = xsk_load_xdp_programs(ifobj);
-	if (err) {
-		ksft_print_msg("Error loading XDP program\n");
-		return err;
-	}
-
 	if (hugepages_present())
-		ifobj->unaligned_supp = true;
+		ifobj_set_cap(ifobj, XSK_CAP_UNALIGNED);
 
 	err = bpf_xdp_query(ifobj->ifindex, XDP_FLAGS_DRV_MODE, &query_opts);
 	if (err) {
@@ -2284,19 +2281,34 @@ int init_iface(struct ifobject *ifobj, thread_func_t func_ptr)
 		return err;
 	}
 	if (query_opts.feature_flags & NETDEV_XDP_ACT_RX_SG)
-		ifobj->multi_buff_supp = true;
+		ifobj_set_cap(ifobj, XSK_CAP_MBUF);
 	if (query_opts.feature_flags & NETDEV_XDP_ACT_XSK_ZEROCOPY) {
 		if (query_opts.xdp_zc_max_segs > 1) {
-			ifobj->multi_buff_zc_supp = true;
-			ifobj->xdp_zc_max_segs = query_opts.xdp_zc_max_segs;
+			ifobj_set_cap(ifobj, XSK_CAP_MBUF_ZC);
+			ifobj->caps.xdp_zc_max_segs = query_opts.xdp_zc_max_segs;
 		} else {
-			ifobj->xdp_zc_max_segs = 0;
+			ifobj->caps.xdp_zc_max_segs = 0;
 		}
 	}
 
 	return 0;
 }
 
+int init_iface(struct ifobject *ifobj, thread_func_t func_ptr)
+{
+	int err;
+
+	ifobj->func_ptr = func_ptr;
+
+	err = xsk_load_xdp_programs(ifobj);
+	if (err) {
+		ksft_print_msg("Error loading XDP program\n");
+		return err;
+	}
+
+	return detect_ifobj_caps(ifobj);
+}
+
 int testapp_send_receive(struct test_spec *test)
 {
 	return testapp_validate_traffic(test);
@@ -2386,7 +2398,7 @@ int testapp_hw_sw_min_ring_size(struct test_spec *test)
 {
 	int ret;
 
-	if (!test->ifobj_tx->hw_ring_size_supp) {
+	if (!ifobj_has_cap(test->ifobj_tx, XSK_CAP_HW_RING)) {
 		ksft_print_msg("HW ring sizes not supported.\n");
 		return TEST_SKIP;
 	}
@@ -2413,7 +2425,7 @@ int testapp_hw_sw_max_ring_size(struct test_spec *test)
 	u32 batch_size;
 	int ret;
 
-	if (!test->ifobj_tx->hw_ring_size_supp) {
+	if (!ifobj_has_cap(test->ifobj_tx, XSK_CAP_HW_RING)) {
 		ksft_print_msg("HW ring sizes not supported.\n");
 		return TEST_SKIP;
 	}
@@ -2436,7 +2448,7 @@ int testapp_hw_sw_max_ring_size(struct test_spec *test)
 	 * updating the Rx HW tail register. __send_pkts() waits until a whole batch fits
 	 * in the XSK TX ring, so keep it below that ring's size on NICs with larger rings.
 	 */
-	batch_size = min(test->ifobj_tx->ring.tx_max_pending, max_descs) - 8;
+	batch_size = min(xsk_get_cap(test, tx_max_pending), max_descs) - 8;
 	test->ifobj_tx->xsk->batch_size = batch_size;
 	test->ifobj_rx->xsk->batch_size = batch_size;
 	if (pkt_stream_replace(test, max_descs, MIN_PKT_SIZE)) {
@@ -2531,7 +2543,7 @@ int testapp_adjust_tail_grow_mb(struct test_spec *test)
 	grow_size = XSK_UMEM__MAX_FRAME_SIZE -
 		    XSK_UMEM__LARGE_FRAME_SIZE -
 		    XDP_PACKET_HEADROOM -
-		    test->ifobj_tx->umem_tailroom;
+		    xsk_get_cap(test, umem_tailroom);
 	test->mtu = MAX_ETH_JUMBO_SIZE;
 
 	return testapp_adjust_tail(test, grow_size, XSK_UMEM__LARGE_FRAME_SIZE * 2);
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index 410702f89dc9..716ea044797c 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -112,8 +112,22 @@ struct xsk_umem_info {
 	bool unaligned_mode;
 };
 
+#define XSK_CAP_UNALIGNED        (1U << 0)
+#define XSK_CAP_MBUF             (1U << 1)
+#define XSK_CAP_MBUF_ZC          (1U << 2)
+#define XSK_CAP_HW_RING          (1U << 3)
+
+struct xsk_caps {
+	u32 flags;
+	u32 xdp_zc_max_segs;
+	u32 max_skb_frags;
+	u32 umem_tailroom;
+	u32 tx_max_pending;
+};
+
 struct ifobject {
 	char ifname[MAX_INTERFACE_NAME_CHARS];
+	struct xsk_caps caps;
 	struct xsk_socket_info *xsk;
 	struct xsk_socket_info *xsk_arr;
 	thread_func_t func_ptr;
@@ -126,9 +140,6 @@ struct ifobject {
 	int ifindex;
 	int mtu;
 	u32 bind_flags;
-	u32 xdp_zc_max_segs;
-	u32 umem_tailroom;
-	u32 max_skb_frags;
 	bool tx_on;
 	bool rx_on;
 	bool use_poll;
@@ -136,11 +147,20 @@ struct ifobject {
 	bool use_fill_ring;
 	bool release_rx;
 	bool use_metadata;
-	bool unaligned_supp;
-	bool multi_buff_supp;
-	bool multi_buff_zc_supp;
-	bool hw_ring_size_supp;
 };
+
+static inline bool ifobj_has_cap(const struct ifobject *ifobj, u32 cap)
+{
+	return ifobj && (ifobj->caps.flags & cap);
+}
+
+static inline void ifobj_set_cap(struct ifobject *ifobj, u32 cap)
+{
+	ifobj->caps.flags |= cap;
+}
+
+#define xsk_get_cap(test, cap) ((test)->ifobj_tx->caps.cap)
+
 struct ifobject *ifobject_create(void);
 void ifobject_delete(struct ifobject *ifobj);
 int init_iface(struct ifobject *ifobj, thread_func_t func_ptr);
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index 7876ad47ed0f..d82224a4fa4b 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -361,15 +361,15 @@ int main(int argc, char **argv)
 		ksft_print_msg("Can't get MAX_SKB_FRAGS from system, using default (17)\n");
 		max_frags = 17;
 	}
-	ifobj_tx->max_skb_frags = max_frags;
-	ifobj_rx->max_skb_frags = max_frags;
+	ifobj_tx->caps.max_skb_frags = max_frags;
+	ifobj_rx->caps.max_skb_frags = max_frags;
 
 	/* 48 bytes is a part of skb_shared_info w/o frags array;
 	 * 16 bytes is sizeof(skb_frag_t)
 	 */
 	umem_tailroom = ALIGN(48 + (max_frags * 16), cache_line_size);
-	ifobj_tx->umem_tailroom = umem_tailroom;
-	ifobj_rx->umem_tailroom = umem_tailroom;
+	ifobj_tx->caps.umem_tailroom = umem_tailroom;
+	ifobj_rx->caps.umem_tailroom = umem_tailroom;
 
 	parse_command_line(ifobj_tx, ifobj_rx, argc, argv);
 
@@ -392,8 +392,10 @@ int main(int argc, char **argv)
 	}
 
 	ret = get_hw_ring_size(ifobj_tx->ifname, &ifobj_tx->ring);
-	if (!ret)
-		ifobj_tx->hw_ring_size_supp = true;
+	if (!ret) {
+		ifobj_set_cap(ifobj_tx, XSK_CAP_HW_RING);
+		ifobj_tx->caps.tx_max_pending = ifobj_tx->ring.tx_max_pending;
+	}
 
 	if (init_iface(ifobj_rx, worker_testapp_validate_rx) ||
 	    init_iface(ifobj_tx, worker_testapp_validate_tx)) {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 08/15] selftests: xsk: split xskxceiver main() into setup, run and cleanup
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (6 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 07/15] selftests: xsk: collect interface capabilities in struct xsk_caps Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation Maciej Fijalkowski
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Prepare xskxceiver for running a single endpoint per process by
untangling main():

- bind_iface() sets up one interface from its -i argument, so that
  interfaces can get their roles after parsing; -l needs none
- detect_mode_caps() and detect_ifobj_caps() record DRV, zero-copy and
  HW ring support as XSK_CAP flags, which mode_supported() checks
- run_pkt_test() returns the result, which main() counts instead of
  test->fail, as not every failing path sets it
- cleanup_iface() detaches and unloads one interface and copes with
  partially initialized objects, as does pkt_stream_delete(NULL), so
  main() has a single exit path; it takes the XDP flags from
  mode_to_xdp_flags(), now exported, so that they match the attach

Call ksft_print_header(), which also switches stdout to line buffering,
right after parsing the command line: xskxceiver never called it, so a
launcher reading through a pipe only saw the output at exit. -l prints
no TAP header and exits with 0 instead of KSFT_XPASS, as listing the
tests is not a test result.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/test_xsk.c  |  10 +-
 .../testing/selftests/net/lib/xsk/test_xsk.h  |   3 +
 .../selftests/net/lib/xsk/xskxceiver.c        | 180 +++++++++---------
 3 files changed, 106 insertions(+), 87 deletions(-)

diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
index edb1feb54c54..76d079d8be23 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -64,7 +64,7 @@ static void gen_eth_hdr(struct xsk_socket_info *xsk, struct ethhdr *eth_hdr)
 	eth_hdr->h_proto = htons(ETH_P_LOOPBACK);
 }
 
-static u32 mode_to_xdp_flags(enum test_mode mode)
+u32 mode_to_xdp_flags(enum test_mode mode)
 {
 	return (mode == TEST_MODE_SKB) ? XDP_FLAGS_SKB_MODE : XDP_FLAGS_DRV_MODE;
 }
@@ -338,6 +338,9 @@ static struct pkt *pkt_stream_get_next_rx_pkt(struct pkt_stream *pkt_stream, u32
 
 void pkt_stream_delete(struct pkt_stream *pkt_stream)
 {
+	if (!pkt_stream)
+		return;
+
 	free(pkt_stream->pkts);
 	free(pkt_stream);
 }
@@ -2291,6 +2294,11 @@ static int detect_ifobj_caps(struct ifobject *ifobj)
 		}
 	}
 
+	if (!get_hw_ring_size(ifobj->ifname, &ifobj->ring)) {
+		ifobj_set_cap(ifobj, XSK_CAP_HW_RING);
+		ifobj->caps.tx_max_pending = ifobj->ring.tx_max_pending;
+	}
+
 	return 0;
 }
 
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index 716ea044797c..8503ef055d37 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -116,6 +116,8 @@ struct xsk_umem_info {
 #define XSK_CAP_MBUF             (1U << 1)
 #define XSK_CAP_MBUF_ZC          (1U << 2)
 #define XSK_CAP_HW_RING          (1U << 3)
+#define XSK_CAP_DRV              (1U << 4)
+#define XSK_CAP_ZC               (1U << 5)
 
 struct xsk_caps {
 	u32 flags;
@@ -164,6 +166,7 @@ static inline void ifobj_set_cap(struct ifobject *ifobj, u32 cap)
 struct ifobject *ifobject_create(void);
 void ifobject_delete(struct ifobject *ifobj);
 int init_iface(struct ifobject *ifobj, thread_func_t func_ptr);
+u32 mode_to_xdp_flags(enum test_mode mode);
 
 int xsk_configure_umem(struct ifobject *ifobj, struct xsk_umem_info *umem, void *buffer, u64 size);
 int xsk_configure_socket(struct xsk_socket_info *xsk, struct xsk_umem_info *umem,
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index d82224a4fa4b..97eece4c57f8 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -81,7 +81,6 @@
 #include <linux/netdev.h>
 #include <linux/ethtool.h>
 #include <linux/align.h>
-#include <linux/kernel.h>
 #include <arpa/inet.h>
 #include <net/if.h>
 #include <locale.h>
@@ -101,7 +100,6 @@
 #include "kselftest.h"
 #include "xsk_xdp_common.h"
 
-static bool opt_print_tests;
 static enum test_mode opt_mode = TEST_MODE_ALL;
 static u32 opt_run_test = RUN_ALL_TESTS;
 
@@ -185,17 +183,40 @@ static void print_usage(char **argv)
 	ksft_exit_xfail();
 }
 
-static bool validate_interface(struct ifobject *ifobj)
+static void bind_iface(struct ifobject *ifobj, const char *ifname, thread_func_t func,
+		       char **argv)
 {
-	if (!strcmp(ifobj->ifname, ""))
-		return false;
-	return true;
+	size_t len = ifname ? strlen(ifname) : 0;
+
+	if (!len || len >= sizeof(ifobj->ifname))
+		print_usage(argv);
+
+	memcpy(ifobj->ifname, ifname, len + 1);
+	ifobj->ifindex = if_nametoindex(ifobj->ifname);
+	if (!ifobj->ifindex) {
+		ksft_print_msg("Error: cannot resolve interface %s\n", ifobj->ifname);
+		ksft_exit_fail();
+	}
+
+	if (init_iface(ifobj, func)) {
+		ksft_print_msg("Error: cannot initialize interface %s\n", ifobj->ifname);
+		ksft_exit_fail();
+	}
+}
+
+static void print_tests(void)
+{
+	u32 i;
+
+	printf("Tests:\n");
+	for (i = 0; i < ARRAY_SIZE(tests); i++)
+		printf("%u: %s\n", i, tests[i].name);
 }
 
 static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj_rx, int argc,
 			       char **argv)
 {
-	struct ifobject *ifobj;
+	const char *ifname[2] = {};
 	u32 interface_nb = 0;
 	int option_index, c;
 
@@ -208,21 +229,10 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 
 		switch (c) {
 		case 'i':
-			if (interface_nb == 0)
-				ifobj = ifobj_tx;
-			else if (interface_nb == 1)
-				ifobj = ifobj_rx;
-			else
+			if (interface_nb >= ARRAY_SIZE(ifname))
 				break;
 
-			memcpy(ifobj->ifname, optarg,
-			       min_t(size_t, MAX_INTERFACE_NAME_CHARS, strlen(optarg)));
-
-			ifobj->ifindex = if_nametoindex(ifobj->ifname);
-			if (!ifobj->ifindex)
-				exit_with_error(errno);
-
-			interface_nb++;
+			ifname[interface_nb++] = optarg;
 			break;
 		case 'v':
 			opt_verbose = true;
@@ -242,7 +252,8 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 				print_usage(argv);
 			break;
 		case 'l':
-			opt_print_tests = true;
+			print_tests();
+			ksft_exit_pass();
 			break;
 		case 't':
 			errno = 0;
@@ -255,6 +266,9 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 			print_usage(argv);
 		}
 	}
+
+	bind_iface(ifobj_tx, ifname[0], worker_testapp_validate_tx, argv);
+	bind_iface(ifobj_rx, ifname[1], worker_testapp_validate_rx, argv);
 }
 
 static void xsk_unload_xdp_programs(struct ifobject *ifobj)
@@ -262,7 +276,7 @@ static void xsk_unload_xdp_programs(struct ifobject *ifobj)
 	xsk_xdp_progs__destroy(ifobj->xdp_progs);
 }
 
-static void run_pkt_test(struct test_spec *test)
+static int run_pkt_test(struct test_spec *test)
 {
 	int ret;
 
@@ -287,6 +301,7 @@ static void run_pkt_test(struct test_spec *test)
 	}
 
 	pkt_stream_restore_default(test);
+	return ret;
 }
 
 static bool is_xdp_supported(int ifindex)
@@ -317,26 +332,43 @@ static bool is_xdp_supported(int ifindex)
 	return true;
 }
 
-static void print_tests(void)
+static u32 detect_mode_caps(struct ifobject *ifobj)
 {
-	u32 i;
+	if (is_xdp_supported(ifobj->ifindex)) {
+		ifobj_set_cap(ifobj, XSK_CAP_DRV);
+		if (ifobj_zc_avail(ifobj))
+			ifobj_set_cap(ifobj, XSK_CAP_ZC);
+	}
 
-	printf("Tests:\n");
-	for (i = 0; i < ARRAY_SIZE(tests); i++)
-		printf("%u: %s\n", i, tests[i].name);
+	return ifobj->caps.flags;
+}
+
+static bool mode_supported(enum test_mode mode, u32 caps)
+{
+	if (mode == TEST_MODE_SKB)
+		return true;
+	if (mode == TEST_MODE_DRV)
+		return caps & XSK_CAP_DRV;
+	return caps & XSK_CAP_ZC;
+}
+
+static void cleanup_iface(struct ifobject *ifobj)
+{
+	if (ifobj->xdp_prog)
+		xsk_detach_xdp_program(ifobj->ifindex,
+				       mode_to_xdp_flags(ifobj->mode));
+	xsk_unload_xdp_programs(ifobj);
 }
 
 int main(int argc, char **argv)
 {
 	u32 cache_line_size, max_frags, umem_tailroom;
 	const size_t total_tests = ARRAY_SIZE(tests);
-	struct pkt_stream *rx_pkt_stream_default;
-	struct pkt_stream *tx_pkt_stream_default;
 	struct ifobject *ifobj_tx, *ifobj_rx;
 	u32 i, j, failed_tests = 0, nb_tests;
-	int modes = TEST_MODE_SKB + 1;
-	struct test_spec test;
-	int ret;
+	struct test_spec test = {};
+	int ret = TEST_FAILURE;
+	u32 caps, modes = 0;
 
 	/* Use libbpf 1.0 API mode */
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
@@ -350,6 +382,10 @@ int main(int argc, char **argv)
 
 	setlocale(LC_ALL, "");
 
+	parse_command_line(ifobj_tx, ifobj_rx, argc, argv);
+	/* Line-buffer stdout so verdicts reach a capturing launcher live. */
+	ksft_print_header();
+
 	cache_line_size = read_procfs_val(SMP_CACHE_BYTES_PATH);
 	if (!cache_line_size) {
 		ksft_print_msg("Can't get SMP_CACHE_BYTES from system, using default (64)\n");
@@ -371,90 +407,62 @@ int main(int argc, char **argv)
 	ifobj_tx->caps.umem_tailroom = umem_tailroom;
 	ifobj_rx->caps.umem_tailroom = umem_tailroom;
 
-	parse_command_line(ifobj_tx, ifobj_rx, argc, argv);
-
-	if (opt_print_tests) {
-		print_tests();
-		ksft_exit_xpass();
-	}
 	if (opt_run_test != RUN_ALL_TESTS && opt_run_test >= total_tests) {
 		ksft_print_msg("Error: test %u does not exist.\n", opt_run_test);
 		ksft_exit_xfail();
 	}
 
-	if (!validate_interface(ifobj_tx) || !validate_interface(ifobj_rx))
-		print_usage(argv);
-
-	if (is_xdp_supported(ifobj_tx->ifindex)) {
-		modes++;
-		if (ifobj_zc_avail(ifobj_tx))
-			modes++;
-	}
+	test.tx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
+	test.rx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
+	if (!test.tx_pkt_stream_default || !test.rx_pkt_stream_default)
+		goto out;
 
-	ret = get_hw_ring_size(ifobj_tx->ifname, &ifobj_tx->ring);
-	if (!ret) {
-		ifobj_set_cap(ifobj_tx, XSK_CAP_HW_RING);
-		ifobj_tx->caps.tx_max_pending = ifobj_tx->ring.tx_max_pending;
-	}
+	caps = detect_mode_caps(ifobj_tx);
 
-	if (init_iface(ifobj_rx, worker_testapp_validate_rx) ||
-	    init_iface(ifobj_tx, worker_testapp_validate_tx)) {
-		ksft_print_msg("Error : can't initialize interfaces\n");
+	if (opt_mode != TEST_MODE_ALL && !mode_supported(opt_mode, caps)) {
+		if (opt_mode == TEST_MODE_DRV)
+			ksft_print_msg("Error: XDP_DRV mode not supported.\n");
+		else
+			ksft_print_msg("Error: zero-copy mode not supported.\n");
 		ksft_exit_xfail();
 	}
 
-	test_init(&test, ifobj_tx, ifobj_rx, 0, &tests[0]);
-	tx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
-	rx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
-	if (!tx_pkt_stream_default || !rx_pkt_stream_default)
-		exit_with_error(ENOMEM);
-	test.tx_pkt_stream_default = tx_pkt_stream_default;
-	test.rx_pkt_stream_default = rx_pkt_stream_default;
-
+	for (i = TEST_MODE_SKB; i <= TEST_MODE_ZC; i++)
+		if ((opt_mode == TEST_MODE_ALL || i == opt_mode) && mode_supported(i, caps))
+			modes++;
 	if (opt_run_test == RUN_ALL_TESTS)
 		nb_tests = total_tests;
 	else
 		nb_tests = 1;
-	if (opt_mode == TEST_MODE_ALL) {
-		ksft_set_plan(modes * nb_tests);
-	} else {
-		if (opt_mode == TEST_MODE_DRV && modes <= TEST_MODE_DRV) {
-			ksft_print_msg("Error: XDP_DRV mode not supported.\n");
-			ksft_exit_xfail();
-		}
-		if (opt_mode == TEST_MODE_ZC && modes <= TEST_MODE_ZC) {
-			ksft_print_msg("Error: zero-copy mode not supported.\n");
-			ksft_exit_xfail();
-		}
+	ksft_set_plan(modes * nb_tests);
 
-		ksft_set_plan(nb_tests);
-	}
-
-	for (i = 0; i < modes; i++) {
+	for (i = TEST_MODE_SKB; i <= TEST_MODE_ZC; i++) {
 		if (opt_mode != TEST_MODE_ALL && i != opt_mode)
 			continue;
+		if (!mode_supported(i, caps))
+			continue;
 
 		for (j = 0; j < total_tests; j++) {
 			if (opt_run_test != RUN_ALL_TESTS && j != opt_run_test)
 				continue;
 
 			test_init(&test, ifobj_tx, ifobj_rx, i, &tests[j]);
-			run_pkt_test(&test);
-			usleep(USLEEP_MAX);
-
-			if (test.fail)
+			if (run_pkt_test(&test) == TEST_FAILURE)
 				failed_tests++;
+			usleep(USLEEP_MAX);
 		}
 	}
+	ret = failed_tests ? TEST_FAILURE : TEST_PASS;
 
-	pkt_stream_delete(tx_pkt_stream_default);
-	pkt_stream_delete(rx_pkt_stream_default);
-	xsk_unload_xdp_programs(ifobj_tx);
-	xsk_unload_xdp_programs(ifobj_rx);
+out:
+	cleanup_iface(ifobj_tx);
+	cleanup_iface(ifobj_rx);
+	pkt_stream_delete(test.tx_pkt_stream_default);
+	pkt_stream_delete(test.rx_pkt_stream_default);
 	ifobject_delete(ifobj_tx);
 	ifobject_delete(ifobj_rx);
 
-	if (failed_tests)
+	if (ret)
 		ksft_exit_fail();
 	else
 		ksft_exit_pass();
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (7 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 08/15] selftests: xsk: split xskxceiver main() into setup, run and cleanup Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-17 17:44   ` sashiko-bot
  2026-10-16 17:43 ` [PATCH v4 net-next 10/15] selftests: xsk: run the RX and TX endpoints in separate processes Maciej Fijalkowski
                   ` (5 subsequent siblings)
  14 siblings, 1 reply; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Replace test_xsk.sh and xsk_prereqs.sh with test_xsk.py, which runs
xskxceiver once per mode and case, so that every case starts from a
fresh process and a crash fails only its own case. A single case is
also the unit that the next patch splits into RX and TX processes.

xskxceiver now requires -m and -t, runs exactly that case, reports one
KTAP result and exits with its verdict. An unsupported mode skips the
case instead of exiting with XFAIL.

test_xsk.py uses the networking selftests Python library, as does the
hardware test added later, so every case is a KTAP case of its own:
test_xsk runs each case that xskxceiver -l lists in skb and drv mode,
e.g. test_xsk.drv_SEND_RECEIVE, and test_xsk_busy_poll runs them again
with busy polling. The usual -t and -T options select cases in place of
the script's -l, -m and -t, and -v and -d go away, as the output of a
run that does not pass is logged. Each case runs on a veth pair of its
own, so an MTU or an XDP program that a crashed case leaves behind
cannot reach the next one. The pair stays in the current network
namespace, as only the initial one has the net.core.max_skb_frags
sysctl that xskxceiver reads. A case is killed and fails after 20s, so
that the run ends within the net timeout even if all cases hang.
xskxceiver is also killed if the wait for it is interrupted, and the
test skips when xskxceiver was not built.

test_xsk.py reserves 32 huge pages of 2 MiB, the size that xskxceiver
maps the UNALIGNED UMEMs with, and restores the count however the run
ends, -l and -h included. When it cannot, as without hugetlb or root,
it restores the count at once and leaves the UNALIGNED cases out. The
huge page, busy-poll and endpoint wait helpers, which the hardware
test shares, go to net/lib/py/xsk.py, which joins the AF_XDP entry in
MAINTAINERS.

test_xsk.py leaves out two cases that test_progs skipped as flaky:

- XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF: bpf_xdp_adjust_tail() frees the
  trimmed fragment as the memory type of the XDP RX queue, which veth
  and generic XDP leave at MEM_TYPE_PAGE_SHARED for frames that they
  copy into page_pool pages, so the page leaks from its pool and the
  pool's shutdown stalls
- XDP_ADJUST_TAIL_GROW: it skips in skb mode, and in drv mode veth
  copies each frame into a page_pool fragment that leaves no room to
  grow the 128-byte frames of the case unless it is the last one of its
  page, so the XDP program drops most frames

Drop the xskxceiver.c note on enabling packet dumps with -D in
test_xsk.sh; xskxceiver has no such option and dumps packets on
validation errors.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 MAINTAINERS                                   |   1 +
 tools/testing/selftests/net/Makefile          |   3 +-
 tools/testing/selftests/net/lib/Makefile      |   2 +-
 .../testing/selftests/net/lib/py/__init__.py  |   4 +
 tools/testing/selftests/net/lib/py/xsk.py     |  73 ++++++
 .../selftests/net/lib/xsk/xskxceiver.c        |  63 ++---
 tools/testing/selftests/net/test_xsk.py       | 139 +++++++++++
 tools/testing/selftests/net/test_xsk.sh       | 230 ------------------
 tools/testing/selftests/net/xsk_prereqs.sh    |  86 -------
 9 files changed, 241 insertions(+), 360 deletions(-)
 create mode 100644 tools/testing/selftests/net/lib/py/xsk.py
 create mode 100755 tools/testing/selftests/net/test_xsk.py
 delete mode 100755 tools/testing/selftests/net/test_xsk.sh
 delete mode 100755 tools/testing/selftests/net/xsk_prereqs.sh

diff --git a/MAINTAINERS b/MAINTAINERS
index 1fc9eaedd744..6985a42bc09c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29784,6 +29784,7 @@ F:	include/uapi/linux/if_xdp.h
 F:	include/uapi/linux/xdp_diag.h
 F:	net/xdp/
 F:	tools/testing/selftests/net/*xsk*
+F:	tools/testing/selftests/net/lib/py/xsk.py
 F:	tools/testing/selftests/net/lib/xsk/
 
 XEN BLOCK SUBSYSTEM
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index aa31b6216dd1..8c8219703bd7 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -118,7 +118,7 @@ TEST_PROGS := \
 	test_vxlan_under_vrf.sh \
 	test_vxlan_vnifilter_notify.sh \
 	test_vxlan_vnifiltering.sh \
-	test_xsk.sh \
+	test_xsk.py \
 	tfo_passive.sh \
 	traceroute.sh \
 	txtimestamp.sh \
@@ -210,7 +210,6 @@ TEST_FILES := \
 	in_netns.sh \
 	lib.sh \
 	settings \
-	xsk_prereqs.sh \
 # end of TEST_FILES
 
 # YNL files, must be before "include ..lib.mk"
diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile
index c26088da0489..5c1612315062 100644
--- a/tools/testing/selftests/net/lib/Makefile
+++ b/tools/testing/selftests/net/lib/Makefile
@@ -34,7 +34,7 @@ include ../../lib.mk
 
 include ../bpf.mk
 
-# AF_XDP test engine used by net/test_xsk.sh.
+# AF_XDP test engine used by net/test_xsk.py.
 XSK_DIR := xsk
 XSK_BPF_OBJ := $(OUTPUT)/xsk_xdp_progs.bpf.o
 XSK_SKEL := $(OUTPUT)/xsk_xdp_progs.skel.h
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index 71df5880b356..1a93ab8b1e60 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -16,6 +16,8 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
     bpftool, ip, ethtool, bpftrace, rand_port, rand_ports, wait_port_listen, \
     ctl_file_write, wait_file, tool, tc
 from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
+from .xsk import xsk_reserve_hugepages, xsk_release_hugepages, \
+    xsk_setup_busy_poll, xsk_wait_endpoint_exit
 from .ynl import NlError, NlctrlFamily, YnlFamily, \
     EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
 from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
@@ -31,6 +33,8 @@ __all__ = ["KSRC",
            "bpftool", "ip", "ethtool", "bpftrace", "rand_port", "rand_ports",
            "wait_port_listen", "ctl_file_write", "wait_file", "tool", "tc",
            "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
+           "xsk_reserve_hugepages", "xsk_release_hugepages",
+           "xsk_setup_busy_poll", "xsk_wait_endpoint_exit",
            "NetdevSim", "NetdevSimDev",
            "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
            "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
diff --git a/tools/testing/selftests/net/lib/py/xsk.py b/tools/testing/selftests/net/lib/py/xsk.py
new file mode 100644
index 000000000000..ffaf4b2d35b9
--- /dev/null
+++ b/tools/testing/selftests/net/lib/py/xsk.py
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Helpers to run xskxceiver, the AF_XDP test program in net/lib/xsk.
+"""
+
+import subprocess
+
+from .ksft import ksft_pr
+from .utils import cmd, ctl_file_write
+
+NR_HUGEPAGES = 32
+# xskxceiver maps the UMEMs of the UNALIGNED cases with MAP_HUGE_2MB.
+HUGEPAGES = "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"
+
+
+def xsk_reserve_hugepages(saved, host=None):
+    """Raise the count of 2 MiB huge pages on host to what the UNALIGNED
+    cases need, and return whether it has that many. Add the count to
+    restore to saved, a list for xsk_release_hugepages(), or restore it
+    right away if the host falls short. A host without the pool or the
+    rights to resize it has no huge pages for the cases."""
+    have = cmd(f"cat {HUGEPAGES}", fail=False, host=host)
+    if have.ret:
+        return False
+    have = int(have.stdout)
+    if have >= NR_HUGEPAGES:
+        return True
+    saved.append((host, have))
+    cmd(f"echo {NR_HUGEPAGES} > {HUGEPAGES}", shell=True, fail=False,
+        host=host)
+    if int(cmd(f"cat {HUGEPAGES}", host=host).stdout) >= NR_HUGEPAGES:
+        return True
+    xsk_release_hugepages([saved.pop()])
+    return False
+
+
+def xsk_release_hugepages(saved):
+    """Restore the counts that xsk_reserve_hugepages() added to saved, the
+    last one first, as two hosts can share the pool."""
+    for host, have in reversed(saved):
+        cmd(f"echo {have} > {HUGEPAGES}", shell=True, fail=False, host=host)
+
+
+def xsk_setup_busy_poll(ifname):
+    """Set napi_defer_hard_irqs and gro_flush_timeout of ifname until the
+    case ends, so that SO_PREFER_BUSY_POLL of xskxceiver -b keeps the IRQs
+    masked."""
+    sysfs = f"/sys/class/net/{ifname}"
+    ctl_file_write(f"{sysfs}/napi_defer_hard_irqs", 2)
+    ctl_file_write(f"{sysfs}/gro_flush_timeout", 200000)
+
+
+def xsk_wait_endpoint_exit(endpoint, label, timeout):
+    """Wait up to timeout seconds for xskxceiver to exit, kill it if it does
+    not or if the wait is interrupted, and return its exit code. Log its
+    output, a KTAP stream of its own, prefixed with label, only if it did
+    not pass."""
+    try:
+        endpoint.process(terminate=False, fail=False, timeout=timeout)
+    except subprocess.TimeoutExpired:
+        endpoint.proc.kill()
+        endpoint.process(terminate=False, fail=False)
+        ksft_pr(f"killed after {timeout}s", line_pfx=f"{label}|")
+    except BaseException:
+        endpoint.proc.kill()
+        endpoint.proc.wait()
+        raise
+    if endpoint.ret:
+        for out in (endpoint.stdout, endpoint.stderr):
+            if out:
+                ksft_pr(out.rstrip(), line_pfx=f"{label}|")
+    return endpoint.ret
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index 97eece4c57f8..b4f9cd371503 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -6,7 +6,7 @@
  * Linux kernel samples/bpf/xdpsock* and modified
  * for use.
  *
- * See test_xsk.sh for detailed information on test topology
+ * See test_xsk.py for detailed information on test topology
  * and prerequisite network setup.
  *
  * This test program contains two threads, each thread is single socket with
@@ -64,11 +64,6 @@
  * - Tx thread Transmits a number of packets from veth<xxxx> to veth<yyyy>
  * - Rx thread verifies if all packets were received and delivered in-order,
  *   and have the right content
- *
- * Enable/disable packet dump mode:
- * --------------------------
- * To enable L2 - L4 headers and payload dump of each packet on STDOUT, add
- * parameter -D to params array in test_xsk.sh, i.e. params=("-S" "-D")
  */
 
 #define _GNU_SOURCE
@@ -169,14 +164,14 @@ static struct option long_options[] = {
 static void print_usage(char **argv)
 {
 	const char *str =
-		"  Usage: xskxceiver [OPTIONS]\n"
+		"  Usage: %s -i TX_IFACE -i RX_IFACE -m MODE -t TEST [OPTIONS]\n"
 		"  Options:\n"
 		"  -i, --interface      Use interface\n"
 		"  -v, --verbose        Verbose output\n"
 		"  -b, --busy-poll      Enable busy poll\n"
-		"  -m, --mode           Run only mode skb, drv, or zc\n"
+		"  -m, --mode           Mode to run: skb, drv or zc\n"
 		"  -l, --list           List all available tests\n"
-		"  -t, --test           Run a specific test. Enter number from -l option.\n"
+		"  -t, --test           Test case to run, a number from -l\n"
 		"  -h, --help           Display this help and exit\n";
 
 	ksft_print_msg(str, basename(argv[0]));
@@ -267,6 +262,9 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 		}
 	}
 
+	if (opt_run_test == RUN_ALL_TESTS || opt_mode == TEST_MODE_ALL)
+		print_usage(argv);
+
 	bind_iface(ifobj_tx, ifname[0], worker_testapp_validate_tx, argv);
 	bind_iface(ifobj_rx, ifname[1], worker_testapp_validate_rx, argv);
 }
@@ -365,10 +363,9 @@ int main(int argc, char **argv)
 	u32 cache_line_size, max_frags, umem_tailroom;
 	const size_t total_tests = ARRAY_SIZE(tests);
 	struct ifobject *ifobj_tx, *ifobj_rx;
-	u32 i, j, failed_tests = 0, nb_tests;
 	struct test_spec test = {};
 	int ret = TEST_FAILURE;
-	u32 caps, modes = 0;
+	u32 caps;
 
 	/* Use libbpf 1.0 API mode */
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
@@ -419,40 +416,18 @@ int main(int argc, char **argv)
 
 	caps = detect_mode_caps(ifobj_tx);
 
-	if (opt_mode != TEST_MODE_ALL && !mode_supported(opt_mode, caps)) {
+	if (!mode_supported(opt_mode, caps)) {
 		if (opt_mode == TEST_MODE_DRV)
-			ksft_print_msg("Error: XDP_DRV mode not supported.\n");
+			ksft_print_msg("XDP_DRV mode not supported.\n");
 		else
-			ksft_print_msg("Error: zero-copy mode not supported.\n");
-		ksft_exit_xfail();
+			ksft_print_msg("zero-copy mode not supported.\n");
+		ret = TEST_SKIP;
+		goto out;
 	}
 
-	for (i = TEST_MODE_SKB; i <= TEST_MODE_ZC; i++)
-		if ((opt_mode == TEST_MODE_ALL || i == opt_mode) && mode_supported(i, caps))
-			modes++;
-	if (opt_run_test == RUN_ALL_TESTS)
-		nb_tests = total_tests;
-	else
-		nb_tests = 1;
-	ksft_set_plan(modes * nb_tests);
-
-	for (i = TEST_MODE_SKB; i <= TEST_MODE_ZC; i++) {
-		if (opt_mode != TEST_MODE_ALL && i != opt_mode)
-			continue;
-		if (!mode_supported(i, caps))
-			continue;
-
-		for (j = 0; j < total_tests; j++) {
-			if (opt_run_test != RUN_ALL_TESTS && j != opt_run_test)
-				continue;
-
-			test_init(&test, ifobj_tx, ifobj_rx, i, &tests[j]);
-			if (run_pkt_test(&test) == TEST_FAILURE)
-				failed_tests++;
-			usleep(USLEEP_MAX);
-		}
-	}
-	ret = failed_tests ? TEST_FAILURE : TEST_PASS;
+	ksft_set_plan(1);
+	test_init(&test, ifobj_tx, ifobj_rx, opt_mode, &tests[opt_run_test]);
+	ret = run_pkt_test(&test);
 
 out:
 	cleanup_iface(ifobj_tx);
@@ -462,6 +437,12 @@ int main(int argc, char **argv)
 	ifobject_delete(ifobj_tx);
 	ifobject_delete(ifobj_rx);
 
+	if (ret == TEST_SKIP && ksft_test_num()) {
+		ksft_print_cnts();
+		return KSFT_SKIP;
+	}
+	if (ret == TEST_SKIP)
+		ksft_exit_skip("mode not supported\n");
 	if (ret)
 		ksft_exit_fail();
 	else
diff --git a/tools/testing/selftests/net/test_xsk.py b/tools/testing/selftests/net/test_xsk.py
new file mode 100755
index 000000000000..80ac67cdebb6
--- /dev/null
+++ b/tools/testing/selftests/net/test_xsk.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+r"""
+AF_XDP tests over veth.
+
+Topology:
+
+                 -----------
+               _ | Process | _
+              /  -----------  \
+             /        |        \
+            /         |         \
+      -----------     |     -----------
+      | Thread1 |     |     | Thread2 |
+      -----------     |     -----------
+           |          |          |
+      -----------     |     -----------
+      |  xskX   |     |     |  xskY   |
+      -----------     |     -----------
+           |          |          |
+      -----------     |     ----------
+      |  vethX  | --------- |  vethY |
+      -----------   peer    ----------
+
+Each case runs an xskxceiver process of its own on a veth pair that the case
+creates in the current network namespace and deletes when it ends.
+
+test_xsk runs every case that "xskxceiver -l" lists but EXCLUDED_CASES in SKB
+and DRV mode, and test_xsk_busy_poll runs them again with busy polling.
+UNALIGNED cases are left out without huge pages. Use -t or -T to select
+cases, e.g. -t 'test_xsk.drv_*'. See lib/xsk/xskxceiver.c for what each case
+tests.
+"""
+
+import random
+import re
+import sys
+from pathlib import Path
+
+from lib.py import (KsftNamedVariant, KsftSkipEx, cmd, defer, ip, ksft_eq,
+                    ksft_exit, ksft_run, ksft_variants, xsk_release_hugepages,
+                    xsk_reserve_hugepages, xsk_setup_busy_poll,
+                    xsk_wait_endpoint_exit)
+
+XSK_BIN = Path(__file__).parent / "lib/xskxceiver"
+CASE_TIMEOUT = 20
+SKIP_CODE = 4
+EXCLUDED_CASES = {
+    # bpf_xdp_adjust_tail() frees a frag that it trims off as the memory type
+    # of the XDP RX queue, which veth and generic XDP leave at
+    # MEM_TYPE_PAGE_SHARED for frames they copy into page_pool pages, so the
+    # page leaks from its pool.
+    "XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF",
+    # The case skips in SKB mode. In DRV mode, veth copies each frame into a
+    # page_pool fragment that leaves no room to grow the 128-byte frames of
+    # the case unless it is the last one of its page, so the XDP program
+    # drops most frames.
+    "XDP_ADJUST_TAIL_GROW",
+}
+
+
+def _get_cases(hugepages):
+    """
+    each test case from xskxceiver -l comes out in a form below:
+    0: SEND_RECEIVE
+    """
+    ret = []
+    out = cmd([str(XSK_BIN), "-l"]).stdout
+    cases = re.findall(r"^(\d+): (\w+)$", out, re.MULTILINE)
+    for mode in ("skb", "drv"):
+        for test_id, name in cases:
+            if name in EXCLUDED_CASES:
+                continue
+            if "UNALIGNED" in name and not hugepages:
+                continue
+            ret.append(KsftNamedVariant(f"{mode}_{name}", mode, int(test_id)))
+    return ret
+
+
+def _veth_pair(busy_poll):
+    pfx = f"xsk{random.randrange(1 << 20):05x}"
+    veths = (pfx + "a", pfx + "b")
+    ip(f"link add {veths[0]} numtxqueues 4 numrxqueues 4 type veth "
+       f"peer name {veths[1]} numtxqueues 4 numrxqueues 4")
+    defer(ip, f"link del {veths[0]}")
+    for dev in veths:
+        # The XDP program redirects all traffic, keep IPv6 off the link.
+        ipv6 = Path("/proc/sys/net/ipv6/conf", dev, "disable_ipv6")
+        if ipv6.exists():
+            ipv6.write_text("1", encoding="utf-8")
+        if busy_poll:
+            xsk_setup_busy_poll(dev)
+    for dev in veths:
+        ip(f"link set {dev} up")
+    return veths
+
+
+def _start(args):
+    return cmd([str(XSK_BIN), *args], background=True, fail=False)
+
+
+def _test(mode, test_id, busy_poll):
+    tx_if, rx_if = _veth_pair(busy_poll)
+    args = ["-m", mode,
+            "-t", str(test_id),
+            *(["-b"] if busy_poll else [])]
+    ret = xsk_wait_endpoint_exit(_start(["-i", tx_if, "-i", rx_if, *args]),
+                                 "xsk", CASE_TIMEOUT)
+    if ret == SKIP_CODE:
+        raise KsftSkipEx("xskxceiver skipped the case")
+    ksft_eq(ret, 0, "xskxceiver exit code")
+
+
+def test_xsk(mode, test_id):
+    _test(mode, test_id, busy_poll=False)
+
+
+def test_xsk_busy_poll(mode, test_id):
+    _test(mode, test_id, busy_poll=True)
+
+
+def main():
+    if not XSK_BIN.exists():
+        print(f"1..0 # SKIP {XSK_BIN} was not built")
+        sys.exit(SKIP_CODE)
+    saved_hugepages = []
+    try:
+        hugepages = xsk_reserve_hugepages(saved_hugepages)
+        cases = _get_cases(hugepages)
+        ksft_run([ksft_variants(cases)(test_xsk),
+                  ksft_variants(cases)(test_xsk_busy_poll)])
+    finally:
+        xsk_release_hugepages(saved_hugepages)
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/testing/selftests/net/test_xsk.sh b/tools/testing/selftests/net/test_xsk.sh
deleted file mode 100755
index fbf4551d596a..000000000000
--- a/tools/testing/selftests/net/test_xsk.sh
+++ /dev/null
@@ -1,230 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: GPL-2.0
-# Copyright(c) 2020 Intel Corporation, Weqaar Janjua <weqaar.a.janjua@intel.com>
-
-# AF_XDP selftests based on veth
-#
-# End-to-end AF_XDP over Veth test
-#
-# Topology:
-# ---------
-#                 -----------
-#               _ | Process | _
-#              /  -----------  \
-#             /        |        \
-#            /         |         \
-#      -----------     |     -----------
-#      | Thread1 |     |     | Thread2 |
-#      -----------     |     -----------
-#           |          |          |
-#      -----------     |     -----------
-#      |  xskX   |     |     |  xskY   |
-#      -----------     |     -----------
-#           |          |          |
-#      -----------     |     ----------
-#      |  vethX  | --------- |  vethY |
-#      -----------   peer    ----------
-#
-# AF_XDP is an address family optimized for high performance packet processing,
-# it is XDP’s user-space interface.
-#
-# An AF_XDP socket is linked to a single UMEM which is a region of virtual
-# contiguous memory, divided into equal-sized frames.
-#
-# Refer to AF_XDP Kernel Documentation for detailed information:
-# https://www.kernel.org/doc/html/latest/networking/af_xdp.html
-#
-# Prerequisites setup by script:
-#
-#   Set up veth interfaces as per the topology shown ^^:
-#   * setup two veth interfaces
-#   ** veth<xxxx>
-#   ** veth<yyyy>
-#   *** xxxx and yyyy are randomly generated 4 digit numbers used to avoid
-#       conflict with any existing interface
-#   * tests the veth and xsk layers of the topology
-#
-# See the source xskxceiver.c for information on each test
-#
-# Kernel configuration:
-# ---------------------
-# See "config" file for recommended kernel config options.
-#
-# Turn on XDP sockets and veth support when compiling i.e.
-# 	Networking support -->
-# 		Networking options -->
-# 			[ * ] XDP sockets
-#
-# Executing Tests:
-# ----------------
-# Must run with CAP_NET_ADMIN capability.
-#
-# Run:
-#   sudo ./test_xsk.sh
-#
-# If running from kselftests:
-#   sudo make run_tests
-#
-# Run with verbose output:
-#   sudo ./test_xsk.sh -v
-#
-# Set up veth interfaces and leave them up so xskxceiver can be launched in a debugger:
-#   sudo ./test_xsk.sh -d
-#
-# Run test suite in a specific mode only [skb,drv,zc]
-#   sudo ./test_xsk.sh -m MODE
-#
-# List available tests
-#   ./test_xsk.sh -l
-#
-# Run a specific test from the test suite
-#   sudo ./test_xsk.sh -t TEST_NAME
-#
-# Display the available command line options
-#   ./test_xsk.sh -h
-
-. xsk_prereqs.sh
-
-while getopts "vdm:lt:h" flag
-do
-	case "${flag}" in
-		v) verbose=1;;
-		d) debug=1;;
-		m) MODE=${OPTARG};;
-		l) list=1;;
-		t) TEST=${OPTARG};;
-		h) help=1;;
-	esac
-done
-
-if [ ! -x "./${XSKOBJ}" ]; then
-	echo "xskxceiver was not built; skipping"
-	exit $ksft_skip
-fi
-
-TEST_NAME="PREREQUISITES"
-
-URANDOM=/dev/urandom
-[ ! -e "${URANDOM}" ] && { echo "${URANDOM} not found. Skipping tests."; test_exit $ksft_fail; }
-
-VETH0_POSTFIX=$(cat ${URANDOM} | tr -dc '0-9' | fold -w 256 | head -n 1 | head --bytes 4)
-VETH0=ve${VETH0_POSTFIX}
-VETH1_POSTFIX=$(cat ${URANDOM} | tr -dc '0-9' | fold -w 256 | head -n 1 | head --bytes 4)
-VETH1=ve${VETH1_POSTFIX}
-MTU=1500
-
-trap ctrl_c INT
-
-function ctrl_c() {
-        cleanup_exit ${VETH0} ${VETH1}
-	exit 1
-}
-
-setup_vethPairs() {
-	if [[ $verbose -eq 1 ]]; then
-	        echo "setting up ${VETH0}"
-	fi
-	ip link add ${VETH0} numtxqueues 4 numrxqueues 4 type veth peer name ${VETH1} numtxqueues 4 numrxqueues 4
-	if [ -f /proc/net/if_inet6 ]; then
-		echo 1 > /proc/sys/net/ipv6/conf/${VETH0}/disable_ipv6
-		echo 1 > /proc/sys/net/ipv6/conf/${VETH1}/disable_ipv6
-	fi
-	if [[ $verbose -eq 1 ]]; then
-	        echo "setting up ${VETH1}"
-	fi
-
-	if [[ $busy_poll -eq 1 ]]; then
-	        echo 2 > /sys/class/net/${VETH0}/napi_defer_hard_irqs
-		echo 200000 > /sys/class/net/${VETH0}/gro_flush_timeout
-		echo 2 > /sys/class/net/${VETH1}/napi_defer_hard_irqs
-		echo 200000 > /sys/class/net/${VETH1}/gro_flush_timeout
-	fi
-
-	ip link set ${VETH1} mtu ${MTU}
-	ip link set ${VETH0} mtu ${MTU}
-	ip link set ${VETH1} up
-	ip link set ${VETH0} up
-}
-
-if [[ $list -eq 1 ]]; then
-        ./${XSKOBJ} -l
-        exit
-fi
-
-if [[ $help -eq 1 ]]; then
-	./${XSKOBJ}
-        exit
-fi
-
-validate_root_exec
-validate_veth_support ${VETH0}
-validate_ip_utility
-setup_vethPairs
-
-retval=$?
-if [ $retval -ne 0 ]; then
-	test_status $retval "${TEST_NAME}"
-	cleanup_exit ${VETH0} ${VETH1}
-	exit $retval
-fi
-
-
-if [[ $verbose -eq 1 ]]; then
-	ARGS+="-v "
-fi
-
-if [ -n "$MODE" ]; then
-	ARGS+="-m ${MODE} "
-fi
-
-if [ -n "$TEST" ]; then
-	ARGS+="-t ${TEST} "
-fi
-
-retval=$?
-test_status $retval "${TEST_NAME}"
-
-## START TESTS
-
-statusList=()
-
-TEST_NAME="XSK_SELFTESTS_${VETH0}_SOFTIRQ"
-
-if [[ $debug -eq 1 ]]; then
-    echo "-i" ${VETH0} "-i" ${VETH1}
-    exit
-fi
-
-exec_xskxceiver
-
-cleanup_exit ${VETH0} ${VETH1}
-
-if [[ $list -eq 1 ]]; then
-    exit
-fi
-
-TEST_NAME="XSK_SELFTESTS_${VETH0}_BUSY_POLL"
-busy_poll=1
-
-setup_vethPairs
-exec_xskxceiver
-
-## END TESTS
-
-cleanup_exit ${VETH0} ${VETH1}
-
-failures=0
-echo -e "\nSummary:"
-for i in "${!statusList[@]}"
-do
-	if [ ${statusList[$i]} -ne 0 ]; then
-	        test_status ${statusList[$i]} ${nameList[$i]}
-		failures=1
-	fi
-done
-
-if [ $failures -eq 0 ]; then
-        echo "All tests successful!"
-else
-	exit 1
-fi
diff --git a/tools/testing/selftests/net/xsk_prereqs.sh b/tools/testing/selftests/net/xsk_prereqs.sh
deleted file mode 100755
index 5e5c8ef3fff2..000000000000
--- a/tools/testing/selftests/net/xsk_prereqs.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: GPL-2.0
-# Copyright(c) 2020 Intel Corporation.
-
-ksft_pass=0
-ksft_fail=1
-ksft_xfail=2
-ksft_xpass=3
-ksft_skip=4
-
-XSKOBJ=lib/xskxceiver
-
-validate_root_exec()
-{
-	msg="skip all tests:"
-	if [ $UID != 0 ]; then
-		echo $msg must be run as root >&2
-		test_exit $ksft_fail
-	else
-		return $ksft_pass
-	fi
-}
-
-validate_veth_support()
-{
-	msg="skip all tests:"
-	if [ $(ip link add $1 type veth 2>/dev/null; echo $?;) != 0 ]; then
-		echo $msg veth kernel support not available >&2
-		test_exit $ksft_skip
-	else
-		ip link del $1
-		return $ksft_pass
-	fi
-}
-
-test_status()
-{
-	statusval=$1
-	if [ $statusval -eq $ksft_fail ]; then
-		echo "$2: [ FAIL ]"
-	elif [ $statusval -eq $ksft_skip ]; then
-		echo "$2: [ SKIPPED ]"
-	elif [ $statusval -eq $ksft_pass ]; then
-		echo "$2: [ PASS ]"
-	fi
-}
-
-test_exit()
-{
-	if [ $1 -ne 0 ]; then
-		test_status $1 $(basename $0)
-	fi
-	exit 1
-}
-
-clear_configs()
-{
-	[ $(ip link show $1 &>/dev/null; echo $?;) == 0 ] &&
-		{ ip link del $1; }
-}
-
-cleanup_exit()
-{
-	clear_configs $1 $2
-}
-
-validate_ip_utility()
-{
-	[ ! $(type -P ip) ] && { echo "'ip' not found. Skipping tests."; test_exit $ksft_skip; }
-}
-
-exec_xskxceiver()
-{
-        if [[ $busy_poll -eq 1 ]]; then
-	        ARGS+="-b "
-	fi
-
-	./${XSKOBJ} -i ${VETH0} -i ${VETH1} ${ARGS}
-	retval=$?
-
-	if [[ $list -ne 1 ]]; then
-	    test_status $retval "${TEST_NAME}"
-	    statusList+=($retval)
-	    nameList+=(${TEST_NAME})
-	fi
-}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 10/15] selftests: xsk: run the RX and TX endpoints in separate processes
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (8 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs Maciej Fijalkowski
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Run the two endpoints of a test case as separate xskxceiver processes,
so that a later patch can run them on two hosts. A process binds only
the interface given with -i, takes its role from -e rx|tx and meets the
other endpoint over a small TCP control channel (-p HOST -P PORT). The
other side is represented by a shadow ifobject that mirrors the local
capabilities and holds a loaded copy of the XDP skeleton, so the shared
test code keeps addressing both sides while only the local interface is
programmed. Once BIDIRECTIONAL swaps the directions, the local
interface runs the shadow's programs.

The pthreads, the barrier, test->fail and -lpthread go away. Each
process runs the worker of its local ifobject, and the endpoints
exchange three fixed-size messages:

- READY, exchanged before each step, works as a barrier; TCP ordering
  puts all progress of the previous step before it
- PROGRESS carries the number of packets RX consumed, which TX
  subtracts from its in-flight count so as not to overrun the RX UMEM
- ABORT tells the other endpoint to stop waiting after a failure

The endpoint given --listen listens and the other one connects,
whichever its role. The listener says on stderr when it listens, so
the launcher starts the connector then rather than polling for the
port, which could also find a foreign listener. The listener waits 30s
for the connector, and -P goes to getaddrinfo() as given. As with the
control channels of other selftests, the hosts are trusted and the
channel is not authenticated. Test selection, interface capabilities,
option ranges and verdicts stay with the launcher.

test_xsk.py starts TX as the listener on a random control port on
127.0.0.1 and RX once TX listens, and kills TX and fails the case when
it does not listen within 30s. The case passes when both endpoints
pass, and is skipped when one skips and neither fails. Only the output
of an endpoint that does not pass is logged, prefixed with its role.
xsk_wait_listen(), which reads the TX stderr up to the listen line,
joins the library helpers, and xsk_wait_endpoint_exit() logs that
output along with the rest.

Reviewed-by: Björn Töpel <bjorn@kernel.org>
Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 tools/testing/selftests/net/lib/Makefile      |   3 +-
 .../testing/selftests/net/lib/py/__init__.py  |   4 +-
 tools/testing/selftests/net/lib/py/xsk.py     |  37 ++-
 .../testing/selftests/net/lib/xsk/test_xsk.c  | 203 +++++++-----
 .../testing/selftests/net/lib/xsk/test_xsk.h  |  23 +-
 .../testing/selftests/net/lib/xsk/xsk_peer.c  | 293 ++++++++++++++++++
 .../testing/selftests/net/lib/xsk/xsk_peer.h  |  19 ++
 .../selftests/net/lib/xsk/xskxceiver.c        | 131 ++++++--
 tools/testing/selftests/net/test_xsk.py       |  68 ++--
 9 files changed, 629 insertions(+), 152 deletions(-)
 create mode 100644 tools/testing/selftests/net/lib/xsk/xsk_peer.c
 create mode 100644 tools/testing/selftests/net/lib/xsk/xsk_peer.h

diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile
index 5c1612315062..49dac776202b 100644
--- a/tools/testing/selftests/net/lib/Makefile
+++ b/tools/testing/selftests/net/lib/Makefile
@@ -38,13 +38,12 @@ include ../bpf.mk
 XSK_DIR := xsk
 XSK_BPF_OBJ := $(OUTPUT)/xsk_xdp_progs.bpf.o
 XSK_SKEL := $(OUTPUT)/xsk_xdp_progs.skel.h
-XSK_SOURCES := $(addprefix $(XSK_DIR)/,xskxceiver.c xsk.c test_xsk.c) \
+XSK_SOURCES := $(addprefix $(XSK_DIR)/,xskxceiver.c xsk.c test_xsk.c xsk_peer.c) \
 	$(top_srcdir)/tools/lib/find_bit.c
 XSK_HEADERS := $(wildcard $(XSK_DIR)/*.h)
 
 PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config
 XSK_LDLIBS := $(shell $(PKG_CONFIG) libelf --libs 2>/dev/null || echo -lelf) -lz
-XSK_LDLIBS += -lpthread
 
 $(XSK_BPF_OBJ): $(XSK_DIR)/xsk_xdp_progs.bpf.c $(XSK_DIR)/xsk_xdp_common.h $(BPFOBJ)
 	$(call msg,BPF_PROG,,$@)
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index 1a93ab8b1e60..a82bac8a6b47 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -17,7 +17,7 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
     ctl_file_write, wait_file, tool, tc
 from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
 from .xsk import xsk_reserve_hugepages, xsk_release_hugepages, \
-    xsk_setup_busy_poll, xsk_wait_endpoint_exit
+    xsk_setup_busy_poll, xsk_wait_endpoint_exit, xsk_wait_listen
 from .ynl import NlError, NlctrlFamily, YnlFamily, \
     EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
 from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
@@ -34,7 +34,7 @@ __all__ = ["KSRC",
            "wait_port_listen", "ctl_file_write", "wait_file", "tool", "tc",
            "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
            "xsk_reserve_hugepages", "xsk_release_hugepages",
-           "xsk_setup_busy_poll", "xsk_wait_endpoint_exit",
+           "xsk_setup_busy_poll", "xsk_wait_endpoint_exit", "xsk_wait_listen",
            "NetdevSim", "NetdevSimDev",
            "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
            "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
diff --git a/tools/testing/selftests/net/lib/py/xsk.py b/tools/testing/selftests/net/lib/py/xsk.py
index ffaf4b2d35b9..7ba7afe10db3 100644
--- a/tools/testing/selftests/net/lib/py/xsk.py
+++ b/tools/testing/selftests/net/lib/py/xsk.py
@@ -5,9 +5,10 @@ Helpers to run xskxceiver, the AF_XDP test program in net/lib/xsk.
 """
 
 import subprocess
+import time
 
 from .ksft import ksft_pr
-from .utils import cmd, ctl_file_write
+from .utils import cmd, ctl_file_write, fd_read_timeout
 
 NR_HUGEPAGES = 32
 # xskxceiver maps the UMEMs of the UNALIGNED cases with MAP_HUGE_2MB.
@@ -51,11 +52,33 @@ def xsk_setup_busy_poll(ifname):
     ctl_file_write(f"{sysfs}/gro_flush_timeout", 200000)
 
 
-def xsk_wait_endpoint_exit(endpoint, label, timeout):
-    """Wait up to timeout seconds for xskxceiver to exit, kill it if it does
-    not or if the wait is interrupted, and return its exit code. Log its
-    output, a KTAP stream of its own, prefixed with label, only if it did
-    not pass."""
+def xsk_wait_listen(endpoint, port, timeout):
+    """Wait up to timeout seconds for an xskxceiver endpoint to print on
+    stderr that it listens on the control port. Unlike polling for the port,
+    this never takes a foreign listener for it and costs no SSH login per
+    try on a remote host. Return the stderr read so far and whether that
+    line came."""
+    line = f"Listening for XSK peer on port {port}\n".encode()
+    fd = endpoint.proc.stderr.fileno()
+    head = b""
+    deadline = time.monotonic() + timeout
+    while line not in head:
+        try:
+            data = fd_read_timeout(fd, max(deadline - time.monotonic(), 0))
+        except TimeoutError:
+            break
+        if not data:
+            break
+        head += data
+    return head.decode(), line in head
+
+
+def xsk_wait_endpoint_exit(endpoint, label, timeout, head=""):
+    """Wait up to timeout seconds for an xskxceiver endpoint to exit, kill it
+    if it does not or if the wait is interrupted, and return its exit code.
+    Log its output, a KTAP stream of its own, prefixed with label, only if
+    it did not pass. head is the start of its stderr, if xsk_wait_listen()
+    took it off the pipe."""
     try:
         endpoint.process(terminate=False, fail=False, timeout=timeout)
     except subprocess.TimeoutExpired:
@@ -67,7 +90,7 @@ def xsk_wait_endpoint_exit(endpoint, label, timeout):
         endpoint.proc.wait()
         raise
     if endpoint.ret:
-        for out in (endpoint.stdout, endpoint.stderr):
+        for out in (endpoint.stdout, head + endpoint.stderr):
             if out:
                 ksft_pr(out.rstrip(), line_pfx=f"{label}|")
     return endpoint.ret
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
index 76d079d8be23..ce88218fef00 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -2,12 +2,12 @@
 #include <arpa/inet.h>
 #include <bpf/bpf.h>
 #include <errno.h>
+#include <limits.h>
 #include <linux/bitmap.h>
 #include <linux/if_link.h>
 #include <linux/mman.h>
 #include <linux/netdev.h>
 #include <poll.h>
-#include <pthread.h>
 #include <string.h>
 #include <sys/mman.h>
 #include <sys/socket.h>
@@ -15,6 +15,7 @@
 #include <unistd.h>
 
 #include "test_xsk.h"
+#include "xsk_peer.h"
 #include "xsk_xdp_common.h"
 #include "xsk_xdp_progs.skel.h"
 
@@ -38,10 +39,40 @@
 static const u8 g_mac[ETH_ALEN] = {0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
 
 bool opt_verbose;
-pthread_barrier_t barr;
-pthread_mutex_t pacing_mutex = PTHREAD_MUTEX_INITIALIZER;
 
 int pkts_in_flight;
+static struct xsk_peer *ctrl_peer;
+
+void xsk_set_endpoint(struct xsk_peer *peer)
+{
+	ctrl_peer = peer;
+}
+
+static int pacing_rx_progress(u32 pkts)
+{
+	int err = xsk_peer_rx_progress(ctrl_peer, pkts);
+
+	if (err)
+		ksft_print_msg("RX control channel failed: %d (%s)\n", err,
+			       strerror(-err));
+	return err;
+}
+
+static int pacing_tx_sync(void)
+{
+	int acked = xsk_peer_tx_sync(ctrl_peer);
+
+	/* Only forged reports can take pkts_in_flight below INT_MIN. */
+	if (acked > 0 && pkts_in_flight < INT_MIN + acked)
+		acked = -EPROTO;
+	if (acked < 0) {
+		ksft_print_msg("TX control channel failed: %d (%s)\n", acked,
+			       strerror(-acked));
+		return acked;
+	}
+	pkts_in_flight -= acked;
+	return 0;
+}
 
 /* The payload is a word consisting of a packet sequence number in the upper
  * 16-bits and a intra packet data sequence number in the lower 16 bits. So the 3rd packet's
@@ -222,7 +253,6 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 	test->current_step = 0;
 	test->total_steps = 1;
 	test->nb_sockets = 1;
-	test->fail = false;
 	test->adjust_tail = false;
 	test->adjust_tail_support = false;
 	test->mtu = MAX_ETH_PKT_SIZE;
@@ -289,26 +319,31 @@ static void test_spec_set_xdp_prog(struct test_spec *test, struct bpf_program *x
 	test->xskmap_tx = xskmap_tx;
 }
 
-static int test_spec_set_mtu(struct test_spec *test, int mtu)
+static int test_spec_set_mtu_ifobj(struct ifobject *ifobj, int mtu)
 {
 	int err;
 
-	if (test->ifobj_rx->mtu != mtu) {
-		err = xsk_set_mtu(test->ifobj_rx->ifindex, mtu);
+	if (ifobj_is_local(ifobj) && ifobj->mtu != mtu) {
+		err = xsk_set_mtu(ifobj->ifindex, mtu);
 		if (err)
 			return err;
-		test->ifobj_rx->mtu = mtu;
-	}
-	if (test->ifobj_tx->mtu != mtu) {
-		err = xsk_set_mtu(test->ifobj_tx->ifindex, mtu);
-		if (err)
-			return err;
-		test->ifobj_tx->mtu = mtu;
 	}
 
+	ifobj->mtu = mtu;
 	return 0;
 }
 
+static int test_spec_set_mtu(struct test_spec *test, int mtu)
+{
+	int err;
+
+	err = test_spec_set_mtu_ifobj(test->ifobj_rx, mtu);
+	if (err)
+		return err;
+
+	return test_spec_set_mtu_ifobj(test->ifobj_tx, mtu);
+}
+
 void pkt_stream_reset(struct pkt_stream *pkt_stream)
 {
 	if (pkt_stream) {
@@ -958,6 +993,9 @@ static int __receive_pkts(struct test_spec *test, struct xsk_socket_info *xsk)
 	fds.fd = xsk_socket__fd(xsk->xsk);
 	fds.events = POLLIN;
 
+	if (pacing_rx_progress(0))
+		return TEST_FAILURE;
+
 	ret = kick_rx(xsk);
 	if (ret)
 		return TEST_FAILURE;
@@ -1057,9 +1095,8 @@ static int __receive_pkts(struct test_spec *test, struct xsk_socket_info *xsk)
 	if (ifobj->release_rx)
 		xsk_ring_cons__release(&xsk->rx, frags_processed);
 
-	pthread_mutex_lock(&pacing_mutex);
-	pkts_in_flight -= pkts_sent;
-	pthread_mutex_unlock(&pacing_mutex);
+	if (pacing_rx_progress(pkts_sent))
+		return TEST_FAILURE;
 	pkts_sent = 0;
 
 	return TEST_CONTINUE;
@@ -1135,9 +1172,18 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 	int ret;
 
 	buffer_len = pkt_get_buffer_len(umem, pkt_stream->max_pkt_len);
+	if (pacing_tx_sync())
+		return TEST_FAILURE;
+
 	/* pkts_in_flight might be negative if many invalid packets are sent */
 	if (pkts_in_flight >= (int)((umem_size(umem) - xsk->batch_size * buffer_len) /
 	    buffer_len) && !test_timeout) {
+		/* Only RX progress reports can lower pkts_in_flight. */
+		if (xsk_peer_closed(ctrl_peer)) {
+			ksft_print_msg("ERROR: [%s] RX exited with %d packets in flight\n",
+				       __func__, pkts_in_flight);
+			return TEST_FAILURE;
+		}
 		ret = kick_tx(xsk);
 		if (ret)
 			return TEST_FAILURE;
@@ -1219,9 +1265,7 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 		valid_frags += nb_frags;
 	}
 
-	pthread_mutex_lock(&pacing_mutex);
 	pkts_in_flight += valid_pkts;
-	pthread_mutex_unlock(&pacing_mutex);
 
 	xsk_ring_prod__submit(&xsk->tx, i);
 	xsk->outstanding_tx += valid_frags;
@@ -1298,9 +1342,6 @@ static int send_pkts(struct test_spec *test, struct ifobject *ifobject)
 			if (ret != TEST_CONTINUE)
 				return ret;
 
-			if (test->fail)
-				return TEST_FAILURE;
-
 			if (!test->poll_tmout) {
 				ret = wait_for_tx_completion(&ifobject->xsk_arr[i]);
 				if (ret)
@@ -1622,36 +1663,57 @@ static int testapp_validate_rx_endpoint(struct test_spec *test,
 	return err;
 }
 
-void *worker_testapp_validate_tx(void *arg)
+static int sync_endpoints(struct test_spec *test)
+{
+	int err = xsk_peer_ready(ctrl_peer);
+
+	if (err)
+		ksft_print_msg("Endpoint synchronization failed at step %u: %d (%s)\n",
+			       test->current_step, err, strerror(-err));
+	return err;
+}
+
+int worker_testapp_validate_tx(struct test_spec *test)
 {
-	struct test_spec *test = (struct test_spec *)arg;
 	int err;
 
-	err = testapp_validate_tx_endpoint(test, test->ifobj_tx);
+	err = sync_endpoints(test);
 	if (err)
-		test->fail = true;
+		return err;
 
-	pthread_exit(NULL);
+	err = testapp_validate_tx_endpoint(test, test->ifobj_tx);
+	if (err) {
+		ksft_print_msg("TX endpoint traffic failed at step %u: %d\n",
+			       test->current_step, err);
+		xsk_peer_abort(ctrl_peer);
+	}
+	return err;
 }
 
-void *worker_testapp_validate_rx(void *arg)
+int worker_testapp_validate_rx(struct test_spec *test)
 {
-	struct test_spec *test = (struct test_spec *)arg;
 	struct ifobject *ifobject = test->ifobj_rx;
 	int err;
 
 	err = testapp_prepare_rx_endpoint(test, ifobject);
+	if (err) {
+		ksft_print_msg("RX endpoint setup failed at step %u: %d\n",
+			       test->current_step, err);
+		xsk_peer_abort(ctrl_peer);
+		return err;
+	}
 
-	if (test->use_barrier)
-		pthread_barrier_wait(&barr);
-
-	/* We leave only now in case of error to avoid getting stuck in the barrier */
-	if (!err)
-		err = testapp_validate_rx_endpoint(test, ifobject);
+	err = sync_endpoints(test);
 	if (err)
-		test->fail = true;
+		return err;
 
-	pthread_exit(NULL);
+	err = testapp_validate_rx_endpoint(test, ifobject);
+	if (err) {
+		ksft_print_msg("RX endpoint traffic failed at step %u: %d\n",
+			       test->current_step, err);
+		xsk_peer_abort(ctrl_peer);
+	}
+	return err;
 }
 
 static void testapp_clean_xsk_umem(struct ifobject *ifobj)
@@ -1706,13 +1768,13 @@ static int xsk_attach_xdp_progs(struct test_spec *test, struct ifobject *ifobj_r
 {
 	int err = 0;
 
-	if (xdp_prog_changed_rx(test)) {
+	if (ifobj_is_local(ifobj_rx) && xdp_prog_changed_rx(test)) {
 		err = xsk_reattach_xdp(ifobj_rx, test->xdp_prog_rx, test->xskmap_rx, test->mode);
 		if (err)
 			return err;
 	}
 
-	if (!ifobj_tx)
+	if (!ifobj_tx || !ifobj_is_local(ifobj_tx))
 		return 0;
 
 	if (xdp_prog_changed_tx(test))
@@ -1725,7 +1787,7 @@ static void clean_sockets(struct test_spec *test, struct ifobject *ifobj)
 {
 	u32 i;
 
-	if (!ifobj || !test)
+	if (!ifobj || !test || !ifobj_is_local(ifobj))
 		return;
 
 	for (i = 0; i < test->nb_sockets; i++)
@@ -1737,15 +1799,15 @@ static void clean_umem(struct test_spec *test, struct ifobject *ifobj1, struct i
 	if (!ifobj1)
 		return;
 
-	testapp_clean_xsk_umem(ifobj1);
-	if (ifobj2)
+	if (ifobj_is_local(ifobj1))
+		testapp_clean_xsk_umem(ifobj1);
+	if (ifobj2 && ifobj_is_local(ifobj2))
 		testapp_clean_xsk_umem(ifobj2);
 }
 
 static int __testapp_validate_traffic(struct test_spec *test, struct ifobject *ifobj1,
 				      struct ifobject *ifobj2)
 {
-	pthread_t t0, t1;
 	u32 mbuf_cap;
 	int err;
 
@@ -1776,51 +1838,27 @@ static int __testapp_validate_traffic(struct test_spec *test, struct ifobject *i
 			       err, strerror(-err));
 		return TEST_FAILURE;
 	}
-	test->use_barrier = !!ifobj2;
-
-	if (test->use_barrier) {
-		if (pthread_barrier_init(&barr, NULL, 2))
-			return TEST_FAILURE;
 
+	if (ifobj2)
 		pkt_stream_reset(ifobj2->xsk->pkt_stream);
-	}
-
 	test->current_step++;
 	pkt_stream_reset(ifobj1->xsk->pkt_stream);
 	pkts_in_flight = 0;
 
-	/*Spawn RX thread */
-	pthread_create(&t0, NULL, ifobj1->func_ptr, test);
-
-	if (test->use_barrier) {
-		pthread_barrier_wait(&barr);
-		if (pthread_barrier_destroy(&barr)) {
-			test->use_barrier = false;
-			pthread_join(t0, NULL);
-			clean_sockets(test, ifobj1);
-			clean_umem(test, ifobj1, NULL);
-			return TEST_FAILURE;
-		}
-	}
-
-	if (ifobj2) {
-		/*Spawn TX thread */
-		pthread_create(&t1, NULL, ifobj2->func_ptr, test);
-		pthread_join(t1, NULL);
-	}
-
-	pthread_join(t0, NULL);
-
-	if (test->total_steps == test->current_step || test->fail) {
+	/* In a one-sided step, the idle endpoint only keeps in step. */
+	if (ifobj_is_local(ifobj1))
+		err = ifobj1->func_ptr(test);
+	else if (ifobj2 && ifobj_is_local(ifobj2))
+		err = ifobj2->func_ptr(test);
+	else
+		err = sync_endpoints(test);
+	if (test->total_steps == test->current_step || err) {
 		clean_sockets(test, ifobj1);
 		clean_sockets(test, ifobj2);
 		clean_umem(test, ifobj1, ifobj2);
 	}
 
-	if (test->fail)
-		return TEST_FAILURE;
-
-	return TEST_PASS;
+	return err ? TEST_FAILURE : TEST_PASS;
 }
 
 static int testapp_validate_traffic(struct test_spec *test)
@@ -1859,7 +1897,7 @@ int testapp_teardown(struct test_spec *test)
 
 static void swap_directions(struct ifobject **ifobj1, struct ifobject **ifobj2)
 {
-	thread_func_t tmp_func_ptr = (*ifobj1)->func_ptr;
+	test_func_t tmp_func_ptr = (*ifobj1)->func_ptr;
 	struct ifobject *tmp_ifobj = (*ifobj1);
 
 	(*ifobj1)->func_ptr = (*ifobj2)->func_ptr;
@@ -1898,6 +1936,9 @@ static int swap_xsk_resources(struct test_spec *test)
 	test->ifobj_tx->xsk = &test->ifobj_tx->xsk_arr[1];
 	test->ifobj_rx->xsk = &test->ifobj_rx->xsk_arr[1];
 
+	if (!ifobj_is_local(test->ifobj_rx))
+		return TEST_PASS;
+
 	ret = xsk_update_xskmap(test->ifobj_rx->xskmap, test->ifobj_rx->xsk->xsk, 0);
 	if (ret)
 		return TEST_FAILURE;
@@ -2246,7 +2287,7 @@ int testapp_too_many_frags(struct test_spec *test)
 	return ret;
 }
 
-static int xsk_load_xdp_programs(struct ifobject *ifobj)
+int xsk_load_xdp_programs(struct ifobject *ifobj)
 {
 	ifobj->xdp_progs = xsk_xdp_progs__open_and_load();
 	if (libbpf_get_error(ifobj->xdp_progs))
@@ -2302,12 +2343,10 @@ static int detect_ifobj_caps(struct ifobject *ifobj)
 	return 0;
 }
 
-int init_iface(struct ifobject *ifobj, thread_func_t func_ptr)
+int init_iface(struct ifobject *ifobj)
 {
 	int err;
 
-	ifobj->func_ptr = func_ptr;
-
 	err = xsk_load_xdp_programs(ifobj);
 	if (err) {
 		ksft_print_msg("Error loading XDP program\n");
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index 8503ef055d37..f1fafae2d140 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -77,9 +77,13 @@ enum test_mode {
 struct ifobject;
 struct test_spec;
 typedef int (*validation_func_t)(struct ifobject *ifobj);
-typedef void *(*thread_func_t)(void *arg);
 typedef int (*test_func_t)(struct test_spec *test);
 
+struct xsk_peer;
+
+/* The control channel to the other endpoint. */
+void xsk_set_endpoint(struct xsk_peer *peer);
+
 struct xsk_socket_info {
 	struct xsk_ring_cons rx;
 	struct xsk_ring_prod tx;
@@ -132,7 +136,7 @@ struct ifobject {
 	struct xsk_caps caps;
 	struct xsk_socket_info *xsk;
 	struct xsk_socket_info *xsk_arr;
-	thread_func_t func_ptr;
+	test_func_t func_ptr;
 	validation_func_t validation_func;
 	struct xsk_xdp_progs *xdp_progs;
 	struct bpf_map *xskmap;
@@ -161,11 +165,18 @@ static inline void ifobj_set_cap(struct ifobject *ifobj, u32 cap)
 	ifobj->caps.flags |= cap;
 }
 
+/* The shadow of the peer's ifobject is never bound to an interface. */
+static inline bool ifobj_is_local(const struct ifobject *ifobj)
+{
+	return ifobj->ifindex;
+}
+
 #define xsk_get_cap(test, cap) ((test)->ifobj_tx->caps.cap)
 
 struct ifobject *ifobject_create(void);
 void ifobject_delete(struct ifobject *ifobj);
-int init_iface(struct ifobject *ifobj, thread_func_t func_ptr);
+int init_iface(struct ifobject *ifobj);
+int xsk_load_xdp_programs(struct ifobject *ifobj);
 u32 mode_to_xdp_flags(enum test_mode mode);
 
 int xsk_configure_umem(struct ifobject *ifobj, struct xsk_umem_info *umem, void *buffer, u64 size);
@@ -215,11 +226,9 @@ struct test_spec {
 	u16 total_steps;
 	u16 current_step;
 	u16 nb_sockets;
-	bool fail;
 	bool adjust_tail;
 	bool adjust_tail_support;
 	bool poll_tmout;
-	bool use_barrier;
 	enum test_mode mode;
 	char name[MAX_TEST_NAME_SIZE];
 };
@@ -280,8 +289,8 @@ int testapp_xdp_metadata_mb(struct test_spec *test);
 int testapp_xdp_prog_cleanup(struct test_spec *test);
 int testapp_xdp_shared_umem(struct test_spec *test);
 
-void *worker_testapp_validate_rx(void *arg);
-void *worker_testapp_validate_tx(void *arg);
+int worker_testapp_validate_rx(struct test_spec *test);
+int worker_testapp_validate_tx(struct test_spec *test);
 
 static const struct test_spec tests[] = {
 	{.name = "SEND_RECEIVE", .test_func = testapp_send_receive},
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_peer.c b/tools/testing/selftests/net/lib/xsk/xsk_peer.c
new file mode 100644
index 000000000000..e3e353d6abd9
--- /dev/null
+++ b/tools/testing/selftests/net/lib/xsk/xsk_peer.c
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <arpa/inet.h>
+#include <errno.h>
+#include <limits.h>
+#include <netdb.h>
+#include <netinet/tcp.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "xsk_peer.h"
+
+#define XSK_PEER_MAGIC			0x58534b50
+#define XSK_PEER_ACCEPT_TMOUT_MS	30000
+
+/*
+ * One case per connection; the launcher owns the verdict. As with the control
+ * channels of other selftests, the test hosts are trusted: nothing
+ * authenticates the peer.
+ */
+enum xsk_peer_msg_type {
+	XSK_PEER_READY = 1,
+	XSK_PEER_PROGRESS,
+	XSK_PEER_ABORT,
+};
+
+struct xsk_peer_msg {
+	u32 magic;
+	u32 type;
+	u32 pkts;
+};
+
+struct xsk_peer {
+	int fd;
+	u32 acked;
+	bool ready_seen;
+	bool closed;
+};
+
+static int io_full(int fd, void *buf, size_t len, bool write_op)
+{
+	size_t done = 0;
+	ssize_t ret;
+
+	while (done < len) {
+		if (write_op)
+			ret = send(fd, (char *)buf + done, len - done, MSG_NOSIGNAL);
+		else
+			ret = read(fd, (char *)buf + done, len - done);
+		if (ret < 0 && errno == EINTR)
+			continue;
+		if (ret < 0)
+			return -errno;
+		if (!ret)
+			return -EPIPE;
+		done += ret;
+	}
+	return 0;
+}
+
+/*
+ * Past its last barrier the peer may exit at any time, and it resets the
+ * connection if our progress reports are still unread.
+ */
+static bool peer_gone(int err)
+{
+	return err == -EPIPE || err == -ECONNRESET;
+}
+
+static int peer_send(struct xsk_peer *peer, u32 type, u32 pkts)
+{
+	struct xsk_peer_msg msg = {
+		.magic = htonl(XSK_PEER_MAGIC),
+		.type = htonl(type),
+		.pkts = htonl(pkts),
+	};
+
+	return io_full(peer->fd, &msg, sizeof(msg), true);
+}
+
+static int peer_recv_one(struct xsk_peer *peer)
+{
+	struct xsk_peer_msg msg;
+	u32 pkts;
+	int err;
+
+	err = io_full(peer->fd, &msg, sizeof(msg), false);
+	if (peer_gone(err))
+		peer->closed = true;
+	if (err)
+		return err;
+	if (ntohl(msg.magic) != XSK_PEER_MAGIC)
+		return -EPROTO;
+
+	switch (ntohl(msg.type)) {
+	case XSK_PEER_READY:
+		/* The peer waits for our READY, so it is at most one step ahead. */
+		if (peer->ready_seen)
+			return -EPROTO;
+		peer->ready_seen = true;
+		return 0;
+	case XSK_PEER_PROGRESS:
+		/* xsk_peer_tx_sync() returns the sum as an int. */
+		pkts = ntohl(msg.pkts);
+		if (pkts > INT_MAX - peer->acked)
+			return -EPROTO;
+		peer->acked += pkts;
+		return 0;
+	case XSK_PEER_ABORT:
+		return -ECANCELED;
+	default:
+		return -EPROTO;
+	}
+}
+
+/*
+ * Consume what the peer has sent so far without blocking. A closed connection
+ * is not an error here, see peer_gone(); xsk_peer_closed() reports it.
+ */
+static int peer_drain(struct xsk_peer *peer)
+{
+	struct pollfd pfd = { .fd = peer->fd, .events = POLLIN };
+	int ret;
+
+	while (!peer->closed) {
+		ret = poll(&pfd, 1, 0);
+		if (ret < 0 && errno == EINTR)
+			continue;
+		if (ret < 0)
+			return -errno;
+		if (!ret)
+			break;
+		ret = peer_recv_one(peer);
+		if (ret && !peer->closed)
+			return ret;
+	}
+	return 0;
+}
+
+/* Return once both endpoints are set up for the next step. */
+int xsk_peer_ready(struct xsk_peer *peer)
+{
+	int err;
+
+	err = peer_send(peer, XSK_PEER_READY, 0);
+	while (!err && !peer->ready_seen)
+		err = peer_recv_one(peer);
+	if (err)
+		return err;
+
+	/* TCP ordering puts all progress of the previous step before READY. */
+	peer->ready_seen = false;
+	peer->acked = 0;
+	return 0;
+}
+
+void xsk_peer_abort(struct xsk_peer *peer)
+{
+	if (peer && !peer->closed)
+		peer_send(peer, XSK_PEER_ABORT, 0);
+}
+
+int xsk_peer_rx_progress(struct xsk_peer *peer, u32 pkts)
+{
+	int err;
+
+	if (pkts && !peer->closed) {
+		err = peer_send(peer, XSK_PEER_PROGRESS, pkts);
+		/*
+		 * Leave closed to peer_drain(), which first reads what the
+		 * peer sent before it went away, such as an ABORT.
+		 */
+		if (err && !peer_gone(err))
+			return err;
+	}
+	return peer_drain(peer);
+}
+
+/* Return the packets RX reported consumed since the last call, or -errno. */
+int xsk_peer_tx_sync(struct xsk_peer *peer)
+{
+	int err = peer_drain(peer);
+	u32 acked = peer->acked;
+
+	if (err)
+		return err;
+	peer->acked = 0;
+	return acked;
+}
+
+bool xsk_peer_closed(struct xsk_peer *peer)
+{
+	return peer->closed;
+}
+
+static int accept_tmout(int lfd)
+{
+	struct pollfd pfd = { .fd = lfd, .events = POLLIN };
+	int ret;
+
+	do {
+		ret = poll(&pfd, 1, XSK_PEER_ACCEPT_TMOUT_MS);
+	} while (ret < 0 && errno == EINTR);
+	if (!ret)
+		errno = ETIMEDOUT;
+	return ret > 0 ? accept(lfd, NULL, NULL) : -1;
+}
+
+/*
+ * The listener says on stderr when it listens, and the launcher waits for
+ * that line before it starts the connector, so connect() needs no retry.
+ * accept() is bounded in case the connector exits before it gets that far.
+ */
+static int peer_open_tcp(const char *host, const char *port, bool listen_side)
+{
+	struct addrinfo hints = {
+		.ai_family = AF_UNSPEC,
+		.ai_socktype = SOCK_STREAM,
+		.ai_flags = AI_NUMERICSERV,
+	};
+	int fd = -1, lfd, saved = ECONNREFUSED, one = 1, err;
+	struct addrinfo *res, *ai;
+
+	err = getaddrinfo(host, port, &hints, &res);
+	if (err)
+		return -EINVAL;
+
+	if (!listen_side)
+		goto conn;
+
+	for (ai = res; ai; ai = ai->ai_next) {
+		lfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (lfd < 0)
+			continue;
+		setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+		if (!bind(lfd, ai->ai_addr, ai->ai_addrlen) && !listen(lfd, 1)) {
+			fprintf(stderr, "Listening for XSK peer on port %s\n", port);
+			fd = accept_tmout(lfd);
+			saved = errno;
+			close(lfd);
+			break;
+		}
+		saved = errno;
+		close(lfd);
+	}
+	freeaddrinfo(res);
+	return fd >= 0 ? fd : -saved;
+
+conn:
+	for (ai = res; ai; ai = ai->ai_next) {
+		fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (fd < 0)
+			continue;
+		if (!connect(fd, ai->ai_addr, ai->ai_addrlen))
+			break;
+		saved = errno;
+		close(fd);
+		fd = -1;
+	}
+	freeaddrinfo(res);
+	return fd >= 0 ? fd : -saved;
+}
+
+struct xsk_peer *xsk_peer_open(const char *host, const char *port,
+			       bool listen_side)
+{
+	int fd = peer_open_tcp(host, port, listen_side);
+	struct xsk_peer *peer;
+	int one = 1;
+
+	if (fd < 0) {
+		errno = -fd;
+		return NULL;
+	}
+	peer = calloc(1, sizeof(*peer));
+	if (!peer) {
+		close(fd);
+		return NULL;
+	}
+	peer->fd = fd;
+	setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+	return peer;
+}
+
+void xsk_peer_close(struct xsk_peer *peer)
+{
+	if (peer) {
+		close(peer->fd);
+		free(peer);
+	}
+}
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_peer.h b/tools/testing/selftests/net/lib/xsk/xsk_peer.h
new file mode 100644
index 000000000000..da4462192994
--- /dev/null
+++ b/tools/testing/selftests/net/lib/xsk/xsk_peer.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef XSK_PEER_H_
+#define XSK_PEER_H_
+
+#include <linux/types.h>
+
+struct xsk_peer;
+
+struct xsk_peer *xsk_peer_open(const char *host, const char *port,
+			       bool listen_side);
+void xsk_peer_close(struct xsk_peer *peer);
+
+int xsk_peer_ready(struct xsk_peer *peer);
+void xsk_peer_abort(struct xsk_peer *peer);
+int xsk_peer_rx_progress(struct xsk_peer *peer, u32 pkts);
+int xsk_peer_tx_sync(struct xsk_peer *peer);
+bool xsk_peer_closed(struct xsk_peer *peer);
+
+#endif /* XSK_PEER_H_ */
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index b4f9cd371503..fbfff586e0e5 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -9,9 +9,9 @@
  * See test_xsk.py for detailed information on test topology
  * and prerequisite network setup.
  *
- * This test program contains two threads, each thread is single socket with
- * a unique UMEM. It validates in-order packet delivery and packet content
- * by sending packets to each other.
+ * Each instance of this test program runs one endpoint, Tx or Rx, with a
+ * single socket and a unique UMEM. Two instances validate in-order packet
+ * delivery and packet content by sending packets to each other.
  *
  * Tests Information:
  * ------------------
@@ -57,12 +57,14 @@
  *
  * Flow:
  * -----
- * - Single process spawns two threads: Tx and Rx
- * - Each of these two threads attach to a veth interface
- * - Each thread creates one AF_XDP socket connected to a unique umem for each
+ * - test_xsk.py starts two processes: Tx and Rx
+ * - Each of these two processes attach to a veth interface
+ * - Each process creates one AF_XDP socket connected to a unique umem for each
  *   veth interface
- * - Tx thread Transmits a number of packets from veth<xxxx> to veth<yyyy>
- * - Rx thread verifies if all packets were received and delivered in-order,
+ * - Tx process listens on a TCP control port (--listen), Rx process connects
+ *   to it and the two synchronize each test step over that connection
+ * - Tx process Transmits a number of packets from veth<xxxx> to veth<yyyy>
+ * - Rx process verifies if all packets were received and delivered in-order,
  *   and have the right content
  */
 
@@ -87,6 +89,7 @@
 #include <sys/types.h>
 
 #include "test_xsk.h"
+#include "xsk_peer.h"
 #include "xsk_xdp_progs.skel.h"
 #include "xsk.h"
 #include "xskxceiver.h"
@@ -95,8 +98,23 @@
 #include "kselftest.h"
 #include "xsk_xdp_common.h"
 
+enum xsk_endpoint_role {
+	XSK_ENDPOINT_NONE,
+	XSK_ENDPOINT_RX,
+	XSK_ENDPOINT_TX,
+};
+
 static enum test_mode opt_mode = TEST_MODE_ALL;
 static u32 opt_run_test = RUN_ALL_TESTS;
+static enum xsk_endpoint_role opt_endpoint_role = XSK_ENDPOINT_NONE;
+static bool opt_listen;
+static const char *opt_peer_host;
+static const char *opt_peer_port;
+static const char *opt_ifname;
+
+enum {
+	OPT_LISTEN = 256,
+};
 
 static void __exit_with_error(int error, const char *file, const char *func, int line)
 {
@@ -157,6 +175,10 @@ static struct option long_options[] = {
 	{"mode", required_argument, 0, 'm'},
 	{"list", no_argument, 0, 'l'},
 	{"test", required_argument, 0, 't'},
+	{"endpoint", required_argument, 0, 'e'},
+	{"peer", required_argument, 0, 'p'},
+	{"peer-port", required_argument, 0, 'P'},
+	{"listen", no_argument, 0, OPT_LISTEN},
 	{"help", no_argument, 0, 'h'},
 	{0, 0, 0, 0}
 };
@@ -164,7 +186,7 @@ static struct option long_options[] = {
 static void print_usage(char **argv)
 {
 	const char *str =
-		"  Usage: %s -i TX_IFACE -i RX_IFACE -m MODE -t TEST [OPTIONS]\n"
+		"  Usage: %s -i IFACE -e rx|tx -m MODE -t TEST -p HOST -P PORT [OPTIONS]\n"
 		"  Options:\n"
 		"  -i, --interface      Use interface\n"
 		"  -v, --verbose        Verbose output\n"
@@ -172,16 +194,19 @@ static void print_usage(char **argv)
 		"  -m, --mode           Mode to run: skb, drv or zc\n"
 		"  -l, --list           List all available tests\n"
 		"  -t, --test           Test case to run, a number from -l\n"
+		"  -e, --endpoint       Role of this endpoint: rx or tx\n"
+		"  -p, --peer           Control host: IPv4/IPv6 address or hostname\n"
+		"  -P, --peer-port      Control TCP port\n"
+		"      --listen         Listen on the control port instead of connecting\n"
 		"  -h, --help           Display this help and exit\n";
 
 	ksft_print_msg(str, basename(argv[0]));
 	ksft_exit_xfail();
 }
 
-static void bind_iface(struct ifobject *ifobj, const char *ifname, thread_func_t func,
-		       char **argv)
+static void bind_iface(struct ifobject *ifobj, const char *ifname, char **argv)
 {
-	size_t len = ifname ? strlen(ifname) : 0;
+	size_t len = strlen(ifname);
 
 	if (!len || len >= sizeof(ifobj->ifname))
 		print_usage(argv);
@@ -193,7 +218,7 @@ static void bind_iface(struct ifobject *ifobj, const char *ifname, thread_func_t
 		ksft_exit_fail();
 	}
 
-	if (init_iface(ifobj, func)) {
+	if (init_iface(ifobj)) {
 		ksft_print_msg("Error: cannot initialize interface %s\n", ifobj->ifname);
 		ksft_exit_fail();
 	}
@@ -211,23 +236,18 @@ static void print_tests(void)
 static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj_rx, int argc,
 			       char **argv)
 {
-	const char *ifname[2] = {};
-	u32 interface_nb = 0;
 	int option_index, c;
 
 	opterr = 0;
 
 	for (;;) {
-		c = getopt_long(argc, argv, "i:vbm:lt:", long_options, &option_index);
+		c = getopt_long(argc, argv, "i:vbm:lt:e:p:P:", long_options, &option_index);
 		if (c == -1)
 			break;
 
 		switch (c) {
 		case 'i':
-			if (interface_nb >= ARRAY_SIZE(ifname))
-				break;
-
-			ifname[interface_nb++] = optarg;
+			opt_ifname = optarg;
 			break;
 		case 'v':
 			opt_verbose = true;
@@ -256,17 +276,33 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 			if (errno)
 				print_usage(argv);
 			break;
+		case 'e':
+			if (!strcmp(optarg, "rx"))
+				opt_endpoint_role = XSK_ENDPOINT_RX;
+			else if (!strcmp(optarg, "tx"))
+				opt_endpoint_role = XSK_ENDPOINT_TX;
+			else
+				print_usage(argv);
+			break;
+		case 'p':
+			opt_peer_host = optarg;
+			break;
+		case 'P':
+			opt_peer_port = optarg;
+			break;
+		case OPT_LISTEN:
+			opt_listen = true;
+			break;
 		case 'h':
 		default:
 			print_usage(argv);
 		}
 	}
 
-	if (opt_run_test == RUN_ALL_TESTS || opt_mode == TEST_MODE_ALL)
+	if (!opt_ifname || opt_endpoint_role == XSK_ENDPOINT_NONE || !opt_peer_host ||
+	    !*opt_peer_host || !opt_peer_port || opt_run_test == RUN_ALL_TESTS ||
+	    opt_mode == TEST_MODE_ALL)
 		print_usage(argv);
-
-	bind_iface(ifobj_tx, ifname[0], worker_testapp_validate_tx, argv);
-	bind_iface(ifobj_rx, ifname[1], worker_testapp_validate_rx, argv);
 }
 
 static void xsk_unload_xdp_programs(struct ifobject *ifobj)
@@ -352,17 +388,46 @@ static bool mode_supported(enum test_mode mode, u32 caps)
 
 static void cleanup_iface(struct ifobject *ifobj)
 {
+	/* A peer shadow has a skeleton but no bound interface. */
+	if (!ifobj_is_local(ifobj))
+		goto unload;
+
 	if (ifobj->xdp_prog)
 		xsk_detach_xdp_program(ifobj->ifindex,
 				       mode_to_xdp_flags(ifobj->mode));
+unload:
 	xsk_unload_xdp_programs(ifobj);
 }
 
+/* Connect the two endpoints without negotiating the remote NIC's capabilities. */
+static int setup_peer(struct ifobject *local, struct ifobject *shadow,
+		      struct xsk_peer **peer)
+{
+	/* Once BIDIRECTIONAL swaps the directions, the local side attaches the
+	 * shadow's programs, so the shadow needs a skeleton of its own.
+	 */
+	if (xsk_load_xdp_programs(shadow))
+		return TEST_FAILURE;
+
+	*peer = xsk_peer_open(opt_peer_host, opt_peer_port, opt_listen);
+	if (!*peer) {
+		ksft_print_msg("Failed to connect XSK peer: %s\n", strerror(errno));
+		return TEST_FAILURE;
+	}
+
+	shadow->caps = local->caps;
+	xsk_set_endpoint(*peer);
+	return TEST_PASS;
+}
+
 int main(int argc, char **argv)
 {
 	u32 cache_line_size, max_frags, umem_tailroom;
 	const size_t total_tests = ARRAY_SIZE(tests);
 	struct ifobject *ifobj_tx, *ifobj_rx;
+	struct ifobject *shadow_ifobj;
+	struct ifobject *local_ifobj;
+	struct xsk_peer *peer = NULL;
 	struct test_spec test = {};
 	int ret = TEST_FAILURE;
 	u32 caps;
@@ -409,12 +474,26 @@ int main(int argc, char **argv)
 		ksft_exit_xfail();
 	}
 
+	/* swap_directions() swaps the workers, so the shadow needs one too. */
+	ifobj_tx->func_ptr = worker_testapp_validate_tx;
+	ifobj_rx->func_ptr = worker_testapp_validate_rx;
+	if (opt_endpoint_role == XSK_ENDPOINT_TX) {
+		local_ifobj = ifobj_tx;
+		shadow_ifobj = ifobj_rx;
+	} else {
+		local_ifobj = ifobj_rx;
+		shadow_ifobj = ifobj_tx;
+	}
+	bind_iface(local_ifobj, opt_ifname, argv);
+	caps = detect_mode_caps(local_ifobj);
+
 	test.tx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
 	test.rx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
 	if (!test.tx_pkt_stream_default || !test.rx_pkt_stream_default)
 		goto out;
 
-	caps = detect_mode_caps(ifobj_tx);
+	if (setup_peer(local_ifobj, shadow_ifobj, &peer))
+		goto out;
 
 	if (!mode_supported(opt_mode, caps)) {
 		if (opt_mode == TEST_MODE_DRV)
@@ -430,8 +509,10 @@ int main(int argc, char **argv)
 	ret = run_pkt_test(&test);
 
 out:
+	xsk_set_endpoint(NULL);
 	cleanup_iface(ifobj_tx);
 	cleanup_iface(ifobj_rx);
+	xsk_peer_close(peer);
 	pkt_stream_delete(test.tx_pkt_stream_default);
 	pkt_stream_delete(test.rx_pkt_stream_default);
 	ifobject_delete(ifobj_tx);
diff --git a/tools/testing/selftests/net/test_xsk.py b/tools/testing/selftests/net/test_xsk.py
index 80ac67cdebb6..7f9d2c04f0e5 100755
--- a/tools/testing/selftests/net/test_xsk.py
+++ b/tools/testing/selftests/net/test_xsk.py
@@ -6,25 +6,22 @@ AF_XDP tests over veth.
 
 Topology:
 
-                 -----------
-               _ | Process | _
-              /  -----------  \
-             /        |        \
-            /         |         \
-      -----------     |     -----------
-      | Thread1 |     |     | Thread2 |
-      -----------     |     -----------
-           |          |          |
-      -----------     |     -----------
-      |  xskX   |     |     |  xskY   |
-      -----------     |     -----------
-           |          |          |
-      -----------     |     ----------
-      |  vethX  | --------- |  vethY |
-      -----------   peer    ----------
-
-Each case runs an xskxceiver process of its own on a veth pair that the case
-creates in the current network namespace and deletes when it ends.
+      -----------   control    -----------
+      | TX proc | ------------ | RX proc |
+      -----------  (TCP, lo)   -----------
+           |                        |
+      -----------              -----------
+      |  xskX   |              |  xskY   |
+      -----------              -----------
+           |                        |
+      -----------              ----------
+      |  vethX  | ------------ |  vethY |
+      -----------     peer     ----------
+
+Each case runs a TX and an RX xskxceiver process on a veth pair that the case
+creates in the current network namespace and deletes when it ends. The TX
+process listens (--listen) on a TCP control port on 127.0.0.1, and the RX
+process connects to it once TX says on stderr that it listens.
 
 test_xsk runs every case that "xskxceiver -l" lists but EXCLUDED_CASES in SKB
 and DRV mode, and test_xsk_busy_poll runs them again with busy polling.
@@ -38,13 +35,16 @@ import re
 import sys
 from pathlib import Path
 
-from lib.py import (KsftNamedVariant, KsftSkipEx, cmd, defer, ip, ksft_eq,
-                    ksft_exit, ksft_run, ksft_variants, xsk_release_hugepages,
-                    xsk_reserve_hugepages, xsk_setup_busy_poll,
-                    xsk_wait_endpoint_exit)
+from lib.py import (KsftFailEx, KsftNamedVariant, KsftSkipEx, cmd, defer, ip,
+                    ksft_exit, ksft_in, ksft_run, ksft_variants, rand_port,
+                    xsk_release_hugepages, xsk_reserve_hugepages,
+                    xsk_setup_busy_poll, xsk_wait_endpoint_exit,
+                    xsk_wait_listen)
 
 XSK_BIN = Path(__file__).parent / "lib/xskxceiver"
 CASE_TIMEOUT = 20
+# xskxceiver waits as long for its peer to connect.
+LISTEN_TIMEOUT = 30
 SKIP_CODE = 4
 EXCLUDED_CASES = {
     # bpf_xdp_adjust_tail() frees a frag that it trims off as the memory type
@@ -102,14 +102,28 @@ def _start(args):
 
 def _test(mode, test_id, busy_poll):
     tx_if, rx_if = _veth_pair(busy_poll)
+    port = rand_port()
     args = ["-m", mode,
             "-t", str(test_id),
+            "-p", "127.0.0.1",
+            "-P", str(port),
             *(["-b"] if busy_poll else [])]
-    ret = xsk_wait_endpoint_exit(_start(["-i", tx_if, "-i", rx_if, *args]),
-                                 "xsk", CASE_TIMEOUT)
-    if ret == SKIP_CODE:
+    tx = _start(["-i", tx_if, "-e", "tx", "--listen", *args])
+    head = ""
+    try:
+        head, listening = xsk_wait_listen(tx, port, LISTEN_TIMEOUT)
+        if not listening:
+            tx.proc.kill()
+            raise KsftFailEx(f"TX endpoint did not listen on port {port}")
+        rx = _start(["-i", rx_if, "-e", "rx", *args])
+        rx_ret = xsk_wait_endpoint_exit(rx, "rx", CASE_TIMEOUT)
+    finally:
+        tx_ret = xsk_wait_endpoint_exit(tx, "tx", CASE_TIMEOUT, head)
+    rets = {tx_ret, rx_ret}
+    if SKIP_CODE in rets and rets <= {0, SKIP_CODE}:
         raise KsftSkipEx("xskxceiver skipped the case")
-    ksft_eq(ret, 0, "xskxceiver exit code")
+    ksft_in(tx_ret, (0, SKIP_CODE), "TX endpoint exit code")
+    ksft_in(rx_ret, (0, SKIP_CODE), "RX endpoint exit code")
 
 
 def test_xsk(mode, test_id):
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (9 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 10/15] selftests: xsk: run the RX and TX endpoints in separate processes Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-17 17:44   ` sashiko-bot
  2026-10-16 17:43 ` [PATCH v4 net-next 12/15] selftests: xsk: prepare xskxceiver for endpoints on two hosts Maciej Fijalkowski
                   ` (3 subsequent siblings)
  14 siblings, 1 reply; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

xskxceiver sends frames of the loopback ethertype between made-up MAC
addresses, and its XDP programs redirect every frame to the XSKMAP.
That suits a veth pair that carries nothing else, but not a physical
link, where the peer NIC filters out the made-up addresses and the
programs take neighbour discovery, LLDP or SSH away from the stack.

Send the test packets as UDP between the MAC addresses of the two
interfaces on any link. Each endpoint takes the IPv4 or IPv6 addresses
of the flow (--src-ip, --dst-ip), the MAC address of its peer
(--dst-mac) and the UDP port (--udp-port), and reads its own MAC
address from the interface. Over IPv4 the UDP checksum is left at zero,
as IPv4 allows; over IPv6 it is computed from the known payload.
PKT_HDR_SIZE and MIN_PKT_SIZE now stand for variables that
xsk_set_flow() sets, so the smallest packet grows with the headers to
64 bytes over IPv4 and 88 over IPv6, and a verbatim stream adds up the
descriptors of a packet for its IP and UDP lengths.

The XDP programs redirect only the UDP packets to the test port and
pass everything else to the stack, all IPv4 fragments included. The port
is written when a skeleton is loaded, so it also reaches the shadow
endpoint's copy, whose programs the local side attaches once
BIDIRECTIONAL swaps the directions. XDP_SHARED_UMEM picks the socket by
the UDP source port, the test port plus the socket index, rather than by
a made-up destination MAC, and xsk_xdp_adjust_tail() numbers a grown
tail after the UDP header.

test_xsk.py sends from 2001:db8::1 to 2001:db8::2 between the MAC
addresses of the veth pair. The veths need not have the addresses, as
the XDP program takes the packets before the stack, and IPv6 stays on.

is_adjust_tail_supported() copied all of .bss into an int with
bpf_map_lookup_elem(), past the end of the int even before the port
grew .bss; read adjust_value through the skeleton instead.
is_frag_valid() now rejects a fragment too short for the words it
reads, and pkt_dump() prints only the words within the length it is
given, which is_pkt_valid() gives as the first fragment's length.
write_payload() zeroes the bytes after the last full word, which the
IPv6 UDP checksum counts.

Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/test_xsk.c  | 240 ++++++++++++++----
 .../testing/selftests/net/lib/xsk/test_xsk.h  |   7 +-
 tools/testing/selftests/net/lib/xsk/xsk.c     |  26 ++
 tools/testing/selftests/net/lib/xsk/xsk.h     |   1 +
 .../selftests/net/lib/xsk/xsk_xdp_common.h    |   3 +-
 .../selftests/net/lib/xsk/xsk_xdp_progs.bpf.c |  91 ++++++-
 .../selftests/net/lib/xsk/xskxceiver.c        |  48 +++-
 tools/testing/selftests/net/test_xsk.py       |  30 ++-
 8 files changed, 368 insertions(+), 78 deletions(-)

diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
index ce88218fef00..9c401a43da43 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -3,10 +3,14 @@
 #include <bpf/bpf.h>
 #include <errno.h>
 #include <limits.h>
+#include <linux/align.h>
 #include <linux/bitmap.h>
 #include <linux/if_link.h>
 #include <linux/mman.h>
 #include <linux/netdev.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/udp.h>
 #include <poll.h>
 #include <string.h>
 #include <sys/mman.h>
@@ -20,14 +24,14 @@
 #include "xsk_xdp_progs.skel.h"
 
 #define DEFAULT_BATCH_SIZE		64
-#define MIN_PKT_SIZE			64
+/* Payload of the smallest packet, which is 64 bytes over IPv4 */
+#define MIN_PAYLOAD_SIZE		20
 #define MAX_ETH_JUMBO_SIZE		9000
 #define MAX_INTERFACES			2
 #define MAX_TEARDOWN_ITER		10
 #define MAX_TX_BUDGET_DEFAULT		32
 #define PKT_DUMP_NB_TO_PRINT		16
-/* Just to align the data in the packet */
-#define PKT_HDR_SIZE			(sizeof(struct ethhdr) + 2)
+#define PKT_HDR_SIZE			pkt_hdr_size
 #define POLL_TMOUT			1000
 #define THREAD_TMOUT			3
 #define UMEM_HEADROOM_TEST_SIZE		128
@@ -36,9 +40,17 @@
 #define XSK_UMEM__LARGE_FRAME_SIZE	(3 * 1024)
 #define XSK_UMEM__MAX_FRAME_SIZE	(4 * 1024)
 
-static const u8 g_mac[ETH_ALEN] = {0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
+/* The test flow, UDP over IPv4 or IPv6 */
+static int flow_family;
+static union {
+	struct in_addr v4;
+	struct in6_addr v6;
+} flow_src, flow_dst;
+static u16 udp_port;
+static u32 pkt_hdr_size;
 
 bool opt_verbose;
+int min_pkt_size;
 
 int pkts_in_flight;
 static struct xsk_peer *ctrl_peer;
@@ -80,19 +92,132 @@ static int pacing_tx_sync(void)
  */
 static void write_payload(void *dest, u32 pkt_nb, u32 start, u32 size)
 {
-	u32 *ptr = (u32 *)dest, i;
+	u32 *ptr = (u32 *)dest, i, words = size / sizeof(*ptr);
 
 	start /= sizeof(*ptr);
-	size /= sizeof(*ptr);
-	for (i = 0; i < size; i++)
+	for (i = 0; i < words; i++)
 		ptr[i] = htonl(pkt_nb << 16 | (i + start));
+	/* The UDP checksum counts a partial last word as zero. */
+	memset(ptr + words, 0, size % sizeof(*ptr));
 }
 
-static void gen_eth_hdr(struct xsk_socket_info *xsk, struct ethhdr *eth_hdr)
+int xsk_set_flow(const char *src_ip, const char *dst_ip, u16 port)
 {
+	u32 ip_hdr_size;
+
+	if (inet_pton(AF_INET, src_ip, &flow_src) == 1 &&
+	    inet_pton(AF_INET, dst_ip, &flow_dst) == 1) {
+		flow_family = AF_INET;
+		ip_hdr_size = sizeof(struct iphdr);
+	} else if (inet_pton(AF_INET6, src_ip, &flow_src) == 1 &&
+		   inet_pton(AF_INET6, dst_ip, &flow_dst) == 1) {
+		flow_family = AF_INET6;
+		ip_hdr_size = sizeof(struct ipv6hdr);
+	} else {
+		return -EINVAL;
+	}
+	if (!port)
+		return -EINVAL;
+
+	udp_port = port;
+	pkt_hdr_size = sizeof(struct ethhdr) + ip_hdr_size +
+		       sizeof(struct udphdr) + PKT_HDR_PAD;
+	/* A multiple of 8, so that a packet that a case puts MIN_PKT_SIZE / 2
+	 * before a boundary keeps its payload 4-byte aligned.
+	 */
+	min_pkt_size = ALIGN(pkt_hdr_size + MIN_PAYLOAD_SIZE, 8);
+	return 0;
+}
+
+/* Add up buf as big-endian 16-bit words, len being even. */
+static u32 csum_add(u32 sum, const void *buf, size_t len)
+{
+	const u8 *pos = buf;
+
+	for (; len > 1; len -= 2, pos += 2)
+		sum += pos[0] << 8 | pos[1];
+	return sum;
+}
+
+static u16 csum_fold(u32 sum)
+{
+	while (sum >> 16)
+		sum = (sum & 0xffff) + (sum >> 16);
+	return htons((u16)~sum);
+}
+
+/* The sum of the payload of len bytes that write_payload() writes: each
+ * word holds pkt_nb and the index of the word in its two 16-bit halves.
+ */
+static u32 payload_csum(u32 pkt_nb, u32 len)
+{
+	u32 words = len / sizeof(u32);
+
+	return words * (pkt_nb & 0xffff) + words * (words - 1) / 2;
+}
+
+static u16 udp6_csum(const struct ipv6hdr *ip6, const struct udphdr *udp, u32 pkt_nb)
+{
+	u32 len = ntohs(udp->len), sum;
+
+	sum = csum_add(0, &ip6->saddr, sizeof(ip6->saddr));
+	sum = csum_add(sum, &ip6->daddr, sizeof(ip6->daddr));
+	sum += len + IPPROTO_UDP;
+	sum = csum_add(sum, udp, sizeof(*udp));
+	/* The pad in front of the payload is zero. */
+	sum += payload_csum(pkt_nb, len - sizeof(*udp) - PKT_HDR_PAD);
+	return csum_fold(sum) ?: 0xffff;
+}
+
+static void gen_pkt_hdr(struct xsk_socket_info *xsk, void *data, u32 pkt_nb, u32 total_len)
+{
+	u32 ip_len = total_len - sizeof(struct ethhdr);
+	struct ethhdr *eth_hdr = data;
+	struct udphdr udp = {
+		.source = htons(xsk->src_port),
+		.dest = htons(udp_port),
+	};
+	void *ptr = eth_hdr + 1;
+
 	memcpy(eth_hdr->h_dest, xsk->dst_mac, ETH_ALEN);
 	memcpy(eth_hdr->h_source, xsk->src_mac, ETH_ALEN);
-	eth_hdr->h_proto = htons(ETH_P_LOOPBACK);
+
+	if (flow_family == AF_INET6) {
+		struct ipv6hdr ip6 = {
+			.version = 6,
+			.payload_len = htons(ip_len - sizeof(ip6)),
+			.nexthdr = IPPROTO_UDP,
+			.hop_limit = 64,
+			.saddr = flow_src.v6,
+			.daddr = flow_dst.v6,
+		};
+
+		eth_hdr->h_proto = htons(ETH_P_IPV6);
+		udp.len = ip6.payload_len;
+		udp.check = udp6_csum(&ip6, &udp, pkt_nb);
+		memcpy(ptr, &ip6, sizeof(ip6));
+		ptr += sizeof(ip6);
+	} else {
+		/* Without a checksum, which UDP over IPv4 does not require */
+		struct iphdr ip = {
+			.version = 4,
+			.ihl = 5,
+			.ttl = 64,
+			.protocol = IPPROTO_UDP,
+			.tot_len = htons(ip_len),
+			.saddr = flow_src.v4.s_addr,
+			.daddr = flow_dst.v4.s_addr,
+		};
+
+		eth_hdr->h_proto = htons(ETH_P_IP);
+		udp.len = htons(ip_len - sizeof(ip));
+		ip.check = csum_fold(csum_add(0, &ip, sizeof(ip)));
+		memcpy(ptr, &ip, sizeof(ip));
+		ptr += sizeof(ip);
+	}
+
+	memcpy(ptr, &udp, sizeof(udp));
+	memset(ptr + sizeof(udp), 0, PKT_HDR_PAD);
 }
 
 u32 mode_to_xdp_flags(enum test_mode mode)
@@ -207,6 +332,7 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 
 	for (i = 0; i < MAX_INTERFACES; i++) {
 		struct ifobject *ifobj = i ? ifobj_rx : ifobj_tx;
+		struct ifobject *peer = i ? ifobj_tx : ifobj_rx;
 		struct xsk_umem_info *umem;
 
 		ifobj->xsk = &ifobj->xsk_arr[0];
@@ -238,10 +364,10 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 			else
 				xsk->pkt_stream = test->rx_pkt_stream_default;
 
-			memcpy(xsk->src_mac, g_mac, ETH_ALEN);
-			memcpy(xsk->dst_mac, g_mac, ETH_ALEN);
-			xsk->src_mac[5] += ((j * 2) + 0);
-			xsk->dst_mac[5] += ((j * 2) + 1);
+			memcpy(xsk->src_mac, ifobj->caps.mac, ETH_ALEN);
+			memcpy(xsk->dst_mac, peer->caps.mac, ETH_ALEN);
+			/* xsk_xdp_shared_umem() picks the socket by it. */
+			xsk->src_port = udp_port + j;
 		}
 
 		ifobj->xsk->umem->num_frames = DEFAULT_UMEM_BUFFERS;
@@ -442,6 +568,19 @@ static u32 pkt_nb_frags(u32 frame_size, struct pkt_stream *pkt_stream, struct pk
 	return nb_frags;
 }
 
+/* A verbatim stream has one entry per descriptor, so add up the packet's. */
+static u32 pkt_total_len(struct pkt_stream *pkt_stream, struct pkt *pkt, u32 nb_frags)
+{
+	u32 i, len = 0;
+
+	if (!pkt_stream->verbatim)
+		return pkt->len;
+
+	for (i = 0; i < nb_frags; i++)
+		len += pkt[i].len;
+	return len;
+}
+
 static bool set_pkt_valid(int offset, u32 len)
 {
 	return len <= MAX_ETH_JUMBO_SIZE;
@@ -626,7 +765,7 @@ static void pkt_stream_cancel(struct pkt_stream *pkt_stream)
 }
 
 static void pkt_generate(struct xsk_socket_info *xsk, struct xsk_umem_info *umem, u64 addr, u32 len,
-			 u32 pkt_nb, u32 bytes_written)
+			 u32 total_len, u32 pkt_nb, u32 bytes_written)
 {
 	void *data = xsk_umem__get_data(umem->buffer, addr);
 
@@ -634,7 +773,7 @@ static void pkt_generate(struct xsk_socket_info *xsk, struct xsk_umem_info *umem
 		return;
 
 	if (!bytes_written) {
-		gen_eth_hdr(xsk, data);
+		gen_pkt_hdr(xsk, data, pkt_nb, total_len);
 
 		len -= PKT_HDR_SIZE;
 		data += PKT_HDR_SIZE;
@@ -727,10 +866,11 @@ static void pkt_print_data(u32 *data, u32 cnt)
 	}
 }
 
+/* With eth_header, len counts the bytes after the test header. */
 static void pkt_dump(void *pkt, u32 len, bool eth_header)
 {
 	struct ethhdr *ethhdr = pkt;
-	u32 i, *data;
+	u32 i, words, *data;
 
 	if (eth_header) {
 		/*extract L2 frame */
@@ -748,12 +888,13 @@ static void pkt_dump(void *pkt, u32 len, bool eth_header)
 	}
 
 	/*extract L5 frame */
+	words = len / sizeof(u32);
 	ksft_print_msg("\nDEBUG>> L5: seqnum: ");
-	pkt_print_data(data, PKT_DUMP_NB_TO_PRINT);
+	pkt_print_data(data, min_t(u32, words, PKT_DUMP_NB_TO_PRINT));
 	ksft_print_msg("....");
-	if (len > PKT_DUMP_NB_TO_PRINT * sizeof(u32)) {
+	if (words > PKT_DUMP_NB_TO_PRINT) {
 		ksft_print_msg("\n.... ");
-		pkt_print_data(data + len / sizeof(u32) - PKT_DUMP_NB_TO_PRINT,
+		pkt_print_data(data + words - PKT_DUMP_NB_TO_PRINT,
 			       PKT_DUMP_NB_TO_PRINT);
 	}
 	ksft_print_msg("\n---------------------------------------\n");
@@ -792,31 +933,12 @@ static bool is_metadata_correct(struct pkt *pkt, void *buffer, u64 addr)
 	return true;
 }
 
-static int is_adjust_tail_supported(struct xsk_xdp_progs *skel_rx, bool *supported)
+static bool is_adjust_tail_supported(struct xsk_xdp_progs *skel_rx)
 {
-	struct bpf_map *data_map;
-	int adjust_value = 0;
-	int key = 0;
-	int ret;
-
-	data_map = bpf_object__find_map_by_name(skel_rx->obj, "xsk_xdp_.bss");
-	if (!data_map || !bpf_map__is_internal(data_map)) {
-		ksft_print_msg("Error: could not find bss section of XDP program\n");
-		return -EINVAL;
-	}
-
-	ret = bpf_map_lookup_elem(bpf_map__fd(data_map), &key, &adjust_value);
-	if (ret) {
-		ksft_print_msg("Error: bpf_map_lookup_elem failed with error %d\n", ret);
-		return ret;
-	}
-
 	/* Set the 'adjust_value' variable to -EOPNOTSUPP in the XDP program if the adjust_tail
 	 * helper is not supported. Skip the adjust_tail test case in this scenario.
 	 */
-	*supported = adjust_value != -EOPNOTSUPP;
-
-	return 0;
+	return skel_rx->bss->adjust_value != -EOPNOTSUPP;
 }
 
 static bool is_frag_valid(struct xsk_umem_info *umem, u64 addr, u32 len, u32 expected_pkt_nb,
@@ -836,6 +958,11 @@ static bool is_frag_valid(struct xsk_umem_info *umem, u64 addr, u32 len, u32 exp
 			       (unsigned long long)addr, len);
 		return false;
 	}
+	if (len < (bytes_processed ? 0 : PKT_HDR_SIZE) + sizeof(*pkt_data)) {
+		ksft_print_msg("Frag too short addr: %llx len: %u\n",
+			       (unsigned long long)addr, len);
+		return false;
+	}
 
 	pkt_data = data;
 	if (!bytes_processed) {
@@ -877,12 +1004,13 @@ static bool is_frag_valid(struct xsk_umem_info *umem, u64 addr, u32 len, u32 exp
 	return false;
 }
 
-static bool is_pkt_valid(struct pkt *pkt, void *buffer, u64 addr, u32 len)
+static bool is_pkt_valid(struct pkt *pkt, void *buffer, u64 addr, u32 frag_len, u32 len)
 {
 	if (pkt->len != len) {
 		ksft_print_msg("[%s] expected packet length [%d], got length [%d]\n",
 			       __func__, pkt->len, len);
-		pkt_dump(xsk_umem__get_data(buffer, addr), len, true);
+		/* Only the first fragment, which holds the header, is at addr. */
+		pkt_dump(xsk_umem__get_data(buffer, addr), frag_len - PKT_HDR_SIZE, true);
 		return false;
 	}
 
@@ -980,7 +1108,7 @@ static int complete_pkts(struct xsk_socket_info *xsk, int batch_size)
 
 static int __receive_pkts(struct test_spec *test, struct xsk_socket_info *xsk)
 {
-	u32 frags_processed = 0, nb_frags = 0, pkt_len = 0;
+	u32 frags_processed = 0, nb_frags = 0, pkt_len = 0, first_len = 0;
 	u32 idx_rx = 0, idx_fq = 0, rcvd, pkts_sent = 0;
 	struct pkt_stream *pkt_stream = xsk->pkt_stream;
 	struct ifobject *ifobj = test->ifobj_rx;
@@ -1057,8 +1185,10 @@ static int __receive_pkts(struct test_spec *test, struct xsk_socket_info *xsk)
 		    !is_metadata_correct(pkt, umem->buffer, addr)))
 			return TEST_FAILURE;
 
-		if (!nb_frags++)
+		if (!nb_frags++) {
 			first_addr = addr;
+			first_len = desc->len;
+		}
 		frags_processed++;
 		pkt_len += desc->len;
 		if (ifobj->use_fill_ring)
@@ -1068,7 +1198,7 @@ static int __receive_pkts(struct test_spec *test, struct xsk_socket_info *xsk)
 			continue;
 
 		/* The complete packet has been received */
-		if (!is_pkt_valid(pkt, umem->buffer, first_addr, pkt_len) ||
+		if (!is_pkt_valid(pkt, umem->buffer, first_addr, first_len, pkt_len) ||
 		    !is_offset_correct(umem, pkt, addr))
 			return TEST_FAILURE;
 
@@ -1218,7 +1348,7 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 
 	for (i = 0; i < xsk->batch_size; i++) {
 		struct pkt *pkt = pkt_stream_get_next_tx_pkt(pkt_stream);
-		u32 nb_frags_left, nb_frags, bytes_written = 0;
+		u32 nb_frags_left, nb_frags, total_len, bytes_written = 0;
 
 		if (!pkt)
 			break;
@@ -1230,6 +1360,7 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 			break;
 		}
 		nb_frags_left = nb_frags;
+		total_len = pkt_total_len(pkt_stream, pkt, nb_frags);
 
 		while (nb_frags_left--) {
 			struct xdp_desc *tx_desc = xsk_ring_prod__tx_desc(&xsk->tx, idx + i);
@@ -1246,8 +1377,8 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 				tx_desc->options = 0;
 			}
 			if (pkt->valid)
-				pkt_generate(xsk, umem, tx_desc->addr, tx_desc->len, pkt->pkt_nb,
-					     bytes_written);
+				pkt_generate(xsk, umem, tx_desc->addr, tx_desc->len, total_len,
+					     pkt->pkt_nb, bytes_written);
 			bytes_written += tx_desc->len;
 
 			print_verbose("Tx addr: %llx len: %u options: %u pkt_nb: %u\n",
@@ -1640,7 +1771,6 @@ static int testapp_prepare_rx_endpoint(struct test_spec *test,
 static int testapp_validate_rx_endpoint(struct test_spec *test,
 					struct ifobject *ifobject)
 {
-	bool supported;
 	int err;
 
 	err = receive_pkts(test);
@@ -1653,9 +1783,7 @@ static int testapp_validate_rx_endpoint(struct test_spec *test,
 	if (!test->adjust_tail)
 		return err;
 
-	if (is_adjust_tail_supported(ifobject->xdp_progs, &supported))
-		return TEST_FAILURE;
-	if (!supported) {
+	if (!is_adjust_tail_supported(ifobject->xdp_progs)) {
 		test->adjust_tail_support = false;
 		return TEST_PASS;
 	}
@@ -2293,6 +2421,8 @@ int xsk_load_xdp_programs(struct ifobject *ifobj)
 	if (libbpf_get_error(ifobj->xdp_progs))
 		return libbpf_get_error(ifobj->xdp_progs);
 
+	/* The shadow's copy too, swapped directions attach its programs. */
+	ifobj->xdp_progs->bss->test_port = udp_port;
 	return 0;
 }
 
@@ -2353,6 +2483,12 @@ int init_iface(struct ifobject *ifobj)
 		return err;
 	}
 
+	err = xsk_get_mac(ifobj->ifname, ifobj->caps.mac);
+	if (err) {
+		ksft_print_msg("Error reading interface MAC address\n");
+		return err;
+	}
+
 	return detect_ifobj_caps(ifobj);
 }
 
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index f1fafae2d140..4690ae2ad9c6 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -24,7 +24,7 @@
 #define DEFAULT_PKT_CNT			(4 * 1024)
 #define DEFAULT_UMEM_BUFFERS		(DEFAULT_PKT_CNT / 4)
 #define HUGEPAGE_SIZE			(2 * 1024 * 1024)
-#define MIN_PKT_SIZE			64
+#define MIN_PKT_SIZE			min_pkt_size
 #define MAX_ETH_PKT_SIZE		1518
 #define MAX_INTERFACE_NAME_CHARS	16
 #define MAX_TEST_NAME_SIZE		48
@@ -35,6 +35,8 @@
 #define SMP_CACHE_BYTES_PATH "/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size"
 
 extern bool opt_verbose;
+/* The smallest test packet, set by xsk_set_flow() */
+extern int min_pkt_size;
 #define print_verbose(x...) do { if (opt_verbose) ksft_print_msg(x); } while (0)
 
 
@@ -83,6 +85,7 @@ struct xsk_peer;
 
 /* The control channel to the other endpoint. */
 void xsk_set_endpoint(struct xsk_peer *peer);
+int xsk_set_flow(const char *src_ip, const char *dst_ip, u16 port);
 
 struct xsk_socket_info {
 	struct xsk_ring_cons rx;
@@ -95,6 +98,7 @@ struct xsk_socket_info {
 	u32 batch_size;
 	u8 dst_mac[ETH_ALEN];
 	u8 src_mac[ETH_ALEN];
+	u16 src_port;
 	bool check_consumer;
 };
 
@@ -129,6 +133,7 @@ struct xsk_caps {
 	u32 max_skb_frags;
 	u32 umem_tailroom;
 	u32 tx_max_pending;
+	u8 mac[ETH_ALEN];
 };
 
 struct ifobject {
diff --git a/tools/testing/selftests/net/lib/xsk/xsk.c b/tools/testing/selftests/net/lib/xsk/xsk.c
index 660540efc46e..54e6808e3fe4 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/xsk.c
@@ -802,3 +802,29 @@ int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param)
 	close(sockfd);
 	return 0;
 }
+
+static int xsk_ifreq_ioctl(const char *ifname, unsigned long req, struct ifreq *ifr)
+{
+	int fd, err = 0;
+
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (fd < 0)
+		return -errno;
+
+	strncpy(ifr->ifr_name, ifname, sizeof(ifr->ifr_name) - 1);
+	if (ioctl(fd, req, ifr) < 0)
+		err = -errno;
+	close(fd);
+	return err;
+}
+
+int xsk_get_mac(const char *ifname, u8 mac[ETH_ALEN])
+{
+	struct ifreq ifr = {};
+	int err;
+
+	err = xsk_ifreq_ioctl(ifname, SIOCGIFHWADDR, &ifr);
+	if (!err)
+		memcpy(mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
+	return err;
+}
diff --git a/tools/testing/selftests/net/lib/xsk/xsk.h b/tools/testing/selftests/net/lib/xsk/xsk.h
index 94a56dd43f2e..263971a57e1e 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/xsk.h
@@ -244,6 +244,7 @@ void xsk_socket__delete(struct xsk_socket *xsk);
 int xsk_set_mtu(int ifindex, int mtu);
 
 int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
+int xsk_get_mac(const char *ifname, u8 mac[ETH_ALEN]);
 
 #ifdef __cplusplus
 } /* extern "C" */
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h b/tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h
index 45810ff552da..47cbceea402d 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h
+++ b/tools/testing/selftests/net/lib/xsk/xsk_xdp_common.h
@@ -4,7 +4,8 @@
 #define XSK_XDP_COMMON_H_
 
 #define MAX_SOCKETS 2
-#define PKT_HDR_ALIGN (sizeof(struct ethhdr) + 2) /* Just to align the data in the packet */
+/* Pads the UDP header of a test packet so that the payload is 4-byte aligned */
+#define PKT_HDR_PAD 2
 
 struct xdp_info {
 	__u64 count;
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c b/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
index 023d8befd4ca..2b55dbaf13b2 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
+++ b/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
@@ -5,9 +5,17 @@
 #include <bpf/bpf_helpers.h>
 #include <linux/if_ether.h>
 #include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/udp.h>
+#include <linux/in.h>
 #include <linux/errno.h>
+#include <bpf/bpf_endian.h>
+#include <stdbool.h>
 #include "xsk_xdp_common.h"
 
+#define IP_MF		0x2000
+#define IP_OFFSET	0x1fff
+
 struct {
 	__uint(type, BPF_MAP_TYPE_XSKMAP);
 	__uint(max_entries, 2);
@@ -15,12 +23,64 @@ struct {
 	__uint(value_size, sizeof(int));
 } xsk SEC(".maps");
 
-static unsigned int idx;
 int adjust_value = 0;
 int count = 0;
+/* UDP destination port of the test flow, other traffic goes to the stack */
+__u16 test_port;
+
+/* Return the UDP header of a test packet, and in hdr_len the size of the
+ * headers in front of its payload, or NULL for any other packet.
+ */
+static __always_inline struct udphdr *test_udp_hdr(struct xdp_md *xdp,
+						   __u32 *hdr_len)
+{
+	void *data_end = (void *)(long)xdp->data_end;
+	void *data = (void *)(long)xdp->data;
+	struct ethhdr *eth = data;
+	struct ipv6hdr *ip6;
+	struct udphdr *udp;
+	struct iphdr *ip;
+
+	if ((void *)(eth + 1) > data_end)
+		return NULL;
+
+	if (eth->h_proto == bpf_htons(ETH_P_IP)) {
+		ip = (void *)(eth + 1);
+		/* Fragments go to the stack, which reassembles them. */
+		if ((void *)(ip + 1) > data_end || ip->ihl != 5 ||
+		    ip->protocol != IPPROTO_UDP ||
+		    ip->frag_off & bpf_htons(IP_MF | IP_OFFSET))
+			return NULL;
+		udp = (void *)(ip + 1);
+		*hdr_len = sizeof(*eth) + sizeof(*ip);
+	} else if (eth->h_proto == bpf_htons(ETH_P_IPV6)) {
+		ip6 = (void *)(eth + 1);
+		/* The test flow carries no extension headers. */
+		if ((void *)(ip6 + 1) > data_end || ip6->nexthdr != IPPROTO_UDP)
+			return NULL;
+		udp = (void *)(ip6 + 1);
+		*hdr_len = sizeof(*eth) + sizeof(*ip6);
+	} else {
+		return NULL;
+	}
+
+	if ((void *)(udp + 1) > data_end || udp->dest != bpf_htons(test_port))
+		return NULL;
+	*hdr_len += sizeof(*udp) + PKT_HDR_PAD;
+	return udp;
+}
+
+static __always_inline bool is_test_packet(struct xdp_md *xdp)
+{
+	__u32 hdr_len;
+
+	return test_udp_hdr(xdp, &hdr_len);
+}
 
 SEC("xdp.frags") int xsk_def_prog(struct xdp_md *xdp)
 {
+	if (!is_test_packet(xdp))
+		return XDP_PASS;
 	return bpf_redirect_map(&xsk, 0, XDP_DROP);
 }
 
@@ -28,6 +88,8 @@ SEC("xdp.frags") int xsk_xdp_drop(struct xdp_md *xdp)
 {
 	static unsigned int drop_idx;
 
+	if (!is_test_packet(xdp))
+		return XDP_PASS;
 	/* Drop every other packet */
 	if (drop_idx++ % 2)
 		return XDP_DROP;
@@ -41,6 +103,9 @@ SEC("xdp.frags") int xsk_xdp_populate_metadata(struct xdp_md *xdp)
 	struct xdp_info *meta;
 	int err;
 
+	if (!is_test_packet(xdp))
+		return XDP_PASS;
+
 	/* Reserve enough for all custom metadata. */
 	err = bpf_xdp_adjust_meta(xdp, -(int)sizeof(struct xdp_info));
 	if (err)
@@ -60,16 +125,17 @@ SEC("xdp.frags") int xsk_xdp_populate_metadata(struct xdp_md *xdp)
 
 SEC("xdp") int xsk_xdp_shared_umem(struct xdp_md *xdp)
 {
-	void *data = (void *)(long)xdp->data;
-	void *data_end = (void *)(long)xdp->data_end;
-	struct ethhdr *eth = data;
+	struct udphdr *udp;
+	__u32 hdr_len;
+	__u16 idx;
 
-	if (eth + 1 > data_end)
-		return XDP_DROP;
+	udp = test_udp_hdr(xdp, &hdr_len);
+	if (!udp)
+		return XDP_PASS;
 
-	/* Redirecting packets based on the destination MAC address */
-	idx = ((unsigned int)(eth->h_dest[5])) / 2;
-	if (idx > MAX_SOCKETS)
+	/* Socket N sends from test_port + N */
+	idx = bpf_ntohs(udp->source) - test_port;
+	if (idx >= MAX_SOCKETS)
 		return XDP_DROP;
 
 	return bpf_redirect_map(&xsk, idx, XDP_DROP);
@@ -77,9 +143,12 @@ SEC("xdp") int xsk_xdp_shared_umem(struct xdp_md *xdp)
 
 SEC("xdp.frags") int xsk_xdp_adjust_tail(struct xdp_md *xdp)
 {
-	__u32 buff_len, curr_buff_len;
+	__u32 buff_len, curr_buff_len, hdr_len;
 	int ret;
 
+	if (!test_udp_hdr(xdp, &hdr_len))
+		return XDP_PASS;
+
 	buff_len = bpf_xdp_get_buff_len(xdp);
 	if (buff_len == 0)
 		return XDP_DROP;
@@ -106,7 +175,7 @@ SEC("xdp.frags") int xsk_xdp_adjust_tail(struct xdp_md *xdp)
 		__u32 *pkt_data = (void *)(long)xdp->data;
 		__u32 len, words_to_end, seq_num;
 
-		len = curr_buff_len - PKT_HDR_ALIGN;
+		len = curr_buff_len - hdr_len;
 		words_to_end = len / sizeof(*pkt_data) - 1;
 		seq_num = words_to_end;
 
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index fbfff586e0e5..9704702f60fc 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -63,7 +63,9 @@
  *   veth interface
  * - Tx process listens on a TCP control port (--listen), Rx process connects
  *   to it and the two synchronize each test step over that connection
- * - Tx process Transmits a number of packets from veth<xxxx> to veth<yyyy>
+ * - Tx process transmits a number of UDP packets over IPv4 or IPv6 from
+ *   veth<xxxx> to veth<yyyy>, and the XDP program of Rx passes other traffic
+ *   to the stack
  * - Rx process verifies if all packets were received and delivered in-order,
  *   and have the right content
  */
@@ -80,6 +82,7 @@
 #include <linux/align.h>
 #include <arpa/inet.h>
 #include <net/if.h>
+#include <netinet/ether.h>
 #include <locale.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -111,9 +114,18 @@ static bool opt_listen;
 static const char *opt_peer_host;
 static const char *opt_peer_port;
 static const char *opt_ifname;
+static const char *opt_src_ip;
+static const char *opt_dst_ip;
+static const struct ether_addr *opt_dst_mac;
+static struct ether_addr dst_mac;
+static u16 opt_udp_port;
 
 enum {
 	OPT_LISTEN = 256,
+	OPT_SRC_IP,
+	OPT_DST_IP,
+	OPT_DST_MAC,
+	OPT_UDP_PORT,
 };
 
 static void __exit_with_error(int error, const char *file, const char *func, int line)
@@ -179,6 +191,10 @@ static struct option long_options[] = {
 	{"peer", required_argument, 0, 'p'},
 	{"peer-port", required_argument, 0, 'P'},
 	{"listen", no_argument, 0, OPT_LISTEN},
+	{"src-ip", required_argument, 0, OPT_SRC_IP},
+	{"dst-ip", required_argument, 0, OPT_DST_IP},
+	{"dst-mac", required_argument, 0, OPT_DST_MAC},
+	{"udp-port", required_argument, 0, OPT_UDP_PORT},
 	{"help", no_argument, 0, 'h'},
 	{0, 0, 0, 0}
 };
@@ -186,7 +202,8 @@ static struct option long_options[] = {
 static void print_usage(char **argv)
 {
 	const char *str =
-		"  Usage: %s -i IFACE -e rx|tx -m MODE -t TEST -p HOST -P PORT [OPTIONS]\n"
+		"  Usage: %s -i IFACE -e rx|tx -m MODE -t TEST -p HOST -P PORT\n"
+		"                    --src-ip IP --dst-ip IP --dst-mac MAC --udp-port PORT [OPTIONS]\n"
 		"  Options:\n"
 		"  -i, --interface      Use interface\n"
 		"  -v, --verbose        Verbose output\n"
@@ -198,6 +215,10 @@ static void print_usage(char **argv)
 		"  -p, --peer           Control host: IPv4/IPv6 address or hostname\n"
 		"  -P, --peer-port      Control TCP port\n"
 		"      --listen         Listen on the control port instead of connecting\n"
+		"      --src-ip IP      IPv4 or IPv6 source address of the test packets\n"
+		"      --dst-ip IP      Destination address, of the same family\n"
+		"      --dst-mac MAC    Destination MAC address of the test packets\n"
+		"      --udp-port PORT  UDP destination port, and source port of socket 0\n"
 		"  -h, --help           Display this help and exit\n";
 
 	ksft_print_msg(str, basename(argv[0]));
@@ -293,6 +314,20 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 		case OPT_LISTEN:
 			opt_listen = true;
 			break;
+		case OPT_SRC_IP:
+			opt_src_ip = optarg;
+			break;
+		case OPT_DST_IP:
+			opt_dst_ip = optarg;
+			break;
+		case OPT_DST_MAC:
+			opt_dst_mac = ether_aton_r(optarg, &dst_mac);
+			if (!opt_dst_mac)
+				print_usage(argv);
+			break;
+		case OPT_UDP_PORT:
+			opt_udp_port = strtoul(optarg, NULL, 10);
+			break;
 		case 'h':
 		default:
 			print_usage(argv);
@@ -301,7 +336,8 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 
 	if (!opt_ifname || opt_endpoint_role == XSK_ENDPOINT_NONE || !opt_peer_host ||
 	    !*opt_peer_host || !opt_peer_port || opt_run_test == RUN_ALL_TESTS ||
-	    opt_mode == TEST_MODE_ALL)
+	    opt_mode == TEST_MODE_ALL || !opt_src_ip || !opt_dst_ip || !opt_dst_mac ||
+	    !opt_udp_port)
 		print_usage(argv);
 }
 
@@ -416,6 +452,8 @@ static int setup_peer(struct ifobject *local, struct ifobject *shadow,
 	}
 
 	shadow->caps = local->caps;
+	memcpy(shadow->caps.mac, opt_dst_mac->ether_addr_octet, ETH_ALEN);
+
 	xsk_set_endpoint(*peer);
 	return TEST_PASS;
 }
@@ -474,6 +512,10 @@ int main(int argc, char **argv)
 		ksft_exit_xfail();
 	}
 
+	/* Before the default packet streams, which have its minimum size. */
+	if (xsk_set_flow(opt_src_ip, opt_dst_ip, opt_udp_port))
+		print_usage(argv);
+
 	/* swap_directions() swaps the workers, so the shadow needs one too. */
 	ifobj_tx->func_ptr = worker_testapp_validate_tx;
 	ifobj_rx->func_ptr = worker_testapp_validate_rx;
diff --git a/tools/testing/selftests/net/test_xsk.py b/tools/testing/selftests/net/test_xsk.py
index 7f9d2c04f0e5..6ca54bafc03d 100755
--- a/tools/testing/selftests/net/test_xsk.py
+++ b/tools/testing/selftests/net/test_xsk.py
@@ -21,7 +21,9 @@ Topology:
 Each case runs a TX and an RX xskxceiver process on a veth pair that the case
 creates in the current network namespace and deletes when it ends. The TX
 process listens (--listen) on a TCP control port on 127.0.0.1, and the RX
-process connects to it once TX says on stderr that it listens.
+process connects to it once TX says on stderr that it listens. The test
+packets are UDP over IPv6 between the MAC addresses of the veth pair, and the
+XDP programs pass other traffic to the stack.
 
 test_xsk runs every case that "xskxceiver -l" lists but EXCLUDED_CASES in SKB
 and DRV mode, and test_xsk_busy_poll runs them again with busy polling.
@@ -46,6 +48,9 @@ CASE_TIMEOUT = 20
 # xskxceiver waits as long for its peer to connect.
 LISTEN_TIMEOUT = 30
 SKIP_CODE = 4
+# Source addresses of the TX and RX endpoints, which the veths do not have.
+TX_ADDR = "2001:db8::1"
+RX_ADDR = "2001:db8::2"
 EXCLUDED_CASES = {
     # bpf_xdp_adjust_tail() frees a frag that it trims off as the memory type
     # of the XDP RX queue, which veth and generic XDP leave at
@@ -53,9 +58,9 @@ EXCLUDED_CASES = {
     # page leaks from its pool.
     "XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF",
     # The case skips in SKB mode. In DRV mode, veth copies each frame into a
-    # page_pool fragment that leaves no room to grow the 128-byte frames of
-    # the case unless it is the last one of its page, so the XDP program
-    # drops most frames.
+    # page_pool fragment that leaves no room to grow the small frames of the
+    # case unless it is the last one of its page, so the XDP program drops
+    # most frames.
     "XDP_ADJUST_TAIL_GROW",
 }
 
@@ -85,10 +90,6 @@ def _veth_pair(busy_poll):
        f"peer name {veths[1]} numtxqueues 4 numrxqueues 4")
     defer(ip, f"link del {veths[0]}")
     for dev in veths:
-        # The XDP program redirects all traffic, keep IPv6 off the link.
-        ipv6 = Path("/proc/sys/net/ipv6/conf", dev, "disable_ipv6")
-        if ipv6.exists():
-            ipv6.write_text("1", encoding="utf-8")
         if busy_poll:
             xsk_setup_busy_poll(dev)
     for dev in veths:
@@ -96,6 +97,10 @@ def _veth_pair(busy_poll):
     return veths
 
 
+def _mac(dev):
+    return ip(f"link show dev {dev}", json=True)[0]["address"]
+
+
 def _start(args):
     return cmd([str(XSK_BIN), *args], background=True, fail=False)
 
@@ -107,15 +112,20 @@ def _test(mode, test_id, busy_poll):
             "-t", str(test_id),
             "-p", "127.0.0.1",
             "-P", str(port),
+            "--udp-port", str(rand_port()),
             *(["-b"] if busy_poll else [])]
-    tx = _start(["-i", tx_if, "-e", "tx", "--listen", *args])
+    tx = _start(["-i", tx_if, "-e", "tx", "--listen",
+                 "--src-ip", TX_ADDR, "--dst-ip", RX_ADDR,
+                 "--dst-mac", _mac(rx_if), *args])
     head = ""
     try:
         head, listening = xsk_wait_listen(tx, port, LISTEN_TIMEOUT)
         if not listening:
             tx.proc.kill()
             raise KsftFailEx(f"TX endpoint did not listen on port {port}")
-        rx = _start(["-i", rx_if, "-e", "rx", *args])
+        rx = _start(["-i", rx_if, "-e", "rx",
+                     "--src-ip", RX_ADDR, "--dst-ip", TX_ADDR,
+                     "--dst-mac", _mac(tx_if), *args])
         rx_ret = xsk_wait_endpoint_exit(rx, "rx", CASE_TIMEOUT)
     finally:
         tx_ret = xsk_wait_endpoint_exit(tx, "tx", CASE_TIMEOUT, head)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 12/15] selftests: xsk: prepare xskxceiver for endpoints on two hosts
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (10 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 13/15] selftests: xsk: list the hardware test cases Maciej Fijalkowski
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

drivers/net/hw/xsk.py, added later in this series, runs each hardware
AF_XDP case with a zero-copy endpoint on the device under test and an
SKB-mode endpoint on a remote host, without negotiating capabilities
between them. Add what such a pair needs:

- --queue binds a queue other than 0
- --max-frags, up to 0xffff as it sizes a packet array, hands both
  endpoints the DUT limit for TOO_MANY_FRAGS, as each one builds the
  packet stream from its own view of it
- --timeout sets the RX and TX loop timeout, 3s by default, and once
  both endpoints are ready, each waits up to as long while its link is
  down, as an MTU or XDP program change on either host can reset a NIC
- an endpoint probes only the mode it runs in, taking zero-copy support
  from NETDEV_XDP_ACT_XSK_ZEROCOPY rather than from a probe bind; its
  XDP_ZEROCOPY bind fails rather than falling back to copy mode, so a
  false advertisement still fails the case
- an endpoint that only transmits attaches the XDP program only in
  zero-copy mode, where a driver can require one for TX wakeups
- an RX endpoint fills its fill ring before the bind, as a zero-copy
  driver posts its RX buffers then and would otherwise wait for the
  first wakeup, which comes only after READY lets the peer send
- an endpoint that skips a case sends SKIP on the control channel, so
  that its peer skips too rather than failing on the closed connection;
  this is how a TX endpoint learns that both XDP_ADJUST_TAIL_GROW cases
  skip on an SKB-mode RX endpoint. It then reads until the peer closes,
  for up to the timeout, as closing with unread data resets the
  connection, which drops a SKIP that the peer has yet to acknowledge

Cases now default to an MTU of 1500 (ETH_DATA_LEN) rather than the
1518 byte frame size: IFLA_MTU counts the L3 MTU, so the old default
changed a standard MTU, which can reset a NIC, for every case. An
SKB-mode endpoint raises its MTU only when a case needs a larger one,
so a jumbo-MTU peer is not reset twice per case. cleanup_iface()
restores the MTU that bind_iface() found once the XDP program, which
can cap it, is detached, and fails the endpoint if it cannot, so the
result is reported after cleanup_iface().

Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/test_xsk.c  | 143 ++++++++++++++----
 .../testing/selftests/net/lib/xsk/test_xsk.h  |   7 +
 tools/testing/selftests/net/lib/xsk/xsk.c     |  34 +++++
 tools/testing/selftests/net/lib/xsk/xsk.h     |   2 +
 .../testing/selftests/net/lib/xsk/xsk_peer.c  |  46 ++++++
 .../testing/selftests/net/lib/xsk/xsk_peer.h  |   2 +
 .../selftests/net/lib/xsk/xskxceiver.c        | 117 +++++++-------
 7 files changed, 271 insertions(+), 80 deletions(-)

diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.c b/tools/testing/selftests/net/lib/xsk/test_xsk.c
index 9c401a43da43..16669f7e0c9f 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.c
@@ -48,6 +48,8 @@ static union {
 } flow_src, flow_dst;
 static u16 udp_port;
 static u32 pkt_hdr_size;
+static u32 max_frags_override;
+static u32 thread_tmout = THREAD_TMOUT;
 
 bool opt_verbose;
 int min_pkt_size;
@@ -129,6 +131,16 @@ int xsk_set_flow(const char *src_ip, const char *dst_ip, u16 port)
 	return 0;
 }
 
+void xsk_set_max_frags(u32 max_frags)
+{
+	max_frags_override = max_frags;
+}
+
+void xsk_set_timeout(u32 timeout)
+{
+	thread_tmout = timeout;
+}
+
 /* Add up buf as big-endian 16-bit words, len being even. */
 static u32 csum_add(u32 sum, const void *buf, size_t len)
 {
@@ -322,7 +334,8 @@ int xsk_configure_socket(struct xsk_socket_info *xsk, struct xsk_umem_info *umem
 
 	txr = ifobject->tx_on ? &xsk->tx : NULL;
 	rxr = ifobject->rx_on ? &xsk->rx : NULL;
-	return xsk_socket__create(&xsk->xsk, ifobject->ifindex, 0, umem->umem, rxr, txr, &cfg);
+	return xsk_socket__create(&xsk->xsk, ifobject->ifindex, ifobject->queue_id,
+				  umem->umem, rxr, txr, &cfg);
 }
 
 static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
@@ -381,7 +394,7 @@ static void __test_spec_init(struct test_spec *test, struct ifobject *ifobj_tx,
 	test->nb_sockets = 1;
 	test->adjust_tail = false;
 	test->adjust_tail_support = false;
-	test->mtu = MAX_ETH_PKT_SIZE;
+	test->mtu = ETH_DATA_LEN;
 	test->xdp_prog_rx = ifobj_rx->xdp_progs->progs.xsk_def_prog;
 	test->xskmap_rx = ifobj_rx->xdp_progs->maps.xsk;
 	test->xdp_prog_tx = ifobj_tx->xdp_progs->progs.xsk_def_prog;
@@ -445,14 +458,25 @@ static void test_spec_set_xdp_prog(struct test_spec *test, struct bpf_program *x
 	test->xskmap_tx = xskmap_tx;
 }
 
-static int test_spec_set_mtu_ifobj(struct ifobject *ifobj, int mtu)
+/* An MTU change can reset a NIC. An SKB-mode endpoint runs a case at any
+ * larger MTU, as generic XDP does not limit it, so only raise its MTU.
+ */
+static bool mtu_change_needed(struct ifobject *ifobj, enum test_mode mode, int mtu)
+{
+	if (mode == TEST_MODE_SKB)
+		return ifobj->dev_mtu < mtu;
+	return ifobj->dev_mtu != mtu;
+}
+
+static int test_spec_set_mtu_ifobj(struct ifobject *ifobj, enum test_mode mode, int mtu)
 {
 	int err;
 
-	if (ifobj_is_local(ifobj) && ifobj->mtu != mtu) {
+	if (ifobj_is_local(ifobj) && mtu_change_needed(ifobj, mode, mtu)) {
 		err = xsk_set_mtu(ifobj->ifindex, mtu);
 		if (err)
 			return err;
+		ifobj->dev_mtu = mtu;
 	}
 
 	ifobj->mtu = mtu;
@@ -463,11 +487,11 @@ static int test_spec_set_mtu(struct test_spec *test, int mtu)
 {
 	int err;
 
-	err = test_spec_set_mtu_ifobj(test->ifobj_rx, mtu);
+	err = test_spec_set_mtu_ifobj(test->ifobj_rx, test->mode, mtu);
 	if (err)
 		return err;
 
-	return test_spec_set_mtu_ifobj(test->ifobj_tx, mtu);
+	return test_spec_set_mtu_ifobj(test->ifobj_tx, test->mode, mtu);
 }
 
 void pkt_stream_reset(struct pkt_stream *pkt_stream)
@@ -1253,7 +1277,7 @@ bool all_packets_received(struct test_spec *test, struct xsk_socket_info *xsk, u
 
 static int receive_pkts(struct test_spec *test)
 {
-	struct timeval tv_end, tv_now, tv_timeout = {THREAD_TMOUT, 0};
+	struct timeval tv_end, tv_now, tv_timeout = {thread_tmout, 0};
 	DECLARE_BITMAP(bitmap, test->nb_sockets);
 	struct xsk_socket_info *xsk;
 	u32 sock_num = 0;
@@ -1309,6 +1333,11 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 	if (pkts_in_flight >= (int)((umem_size(umem) - xsk->batch_size * buffer_len) /
 	    buffer_len) && !test_timeout) {
 		/* Only RX progress reports can lower pkts_in_flight. */
+		if (xsk_peer_skipped(ctrl_peer)) {
+			ksft_print_msg("RX skipped the case with %d packets in flight\n",
+				       pkts_in_flight);
+			return TEST_SKIP;
+		}
 		if (xsk_peer_closed(ctrl_peer)) {
 			ksft_print_msg("ERROR: [%s] RX exited with %d packets in flight\n",
 				       __func__, pkts_in_flight);
@@ -1425,7 +1454,7 @@ static int __send_pkts(struct ifobject *ifobject, struct xsk_socket_info *xsk,
 
 static int wait_for_tx_completion(struct xsk_socket_info *xsk)
 {
-	struct timeval tv_end, tv_now, tv_timeout = {THREAD_TMOUT, 0};
+	struct timeval tv_end, tv_now, tv_timeout = {thread_tmout, 0};
 	int ret;
 
 	ret = gettimeofday(&tv_now, NULL);
@@ -1708,20 +1737,26 @@ static int thread_common_ops(struct test_spec *test, struct ifobject *ifobject)
 	if (ret)
 		return ret;
 
+	ifobject->xsk = &ifobject->xsk_arr[0];
+
+	/* Before the bind: a zero-copy driver posts its RX buffers then, or in
+	 * a poll it schedules then. Otherwise it waits for a wakeup, and the
+	 * peer can send before the RX endpoint gets to it.
+	 */
+	if (ifobject->rx_on) {
+		ret = xsk_populate_fill_ring(umem, ifobject->xsk->pkt_stream,
+					     ifobject->use_fill_ring);
+		if (ret)
+			return ret;
+	}
+
 	ret = xsk_configure(test, ifobject, umem);
 	if (ret)
 		return ret;
 
-	ifobject->xsk = &ifobject->xsk_arr[0];
-
 	if (!ifobject->rx_on)
 		return 0;
 
-	ret = xsk_populate_fill_ring(umem, ifobject->xsk->pkt_stream,
-				     ifobject->use_fill_ring);
-	if (ret)
-		return ret;
-
 	for (i = 0; i < test->nb_sockets; i++) {
 		ret = xsk_update_xskmap(ifobject->xskmap, ifobject->xsk_arr[i].xsk, i);
 		if (ret)
@@ -1791,14 +1826,43 @@ static int testapp_validate_rx_endpoint(struct test_spec *test,
 	return err;
 }
 
+/* An MTU or XDP program change can reset a NIC, which takes the link down on
+ * both hosts. Each endpoint makes those changes before it is ready, so once
+ * both are, wait while the local link is down. This is best effort: it misses
+ * a reset that a driver has yet to start, and one on the bind of a TX socket,
+ * which comes after the wait.
+ */
+static int wait_link_up(struct test_spec *test)
+{
+	struct ifobject *ifobj = test->ifobj_tx;
+	int err;
+
+	if (!ifobj_is_local(ifobj))
+		ifobj = test->ifobj_rx;
+	err = xsk_wait_link_up(ifobj->ifname, thread_tmout * 1000);
+	if (err) {
+		ksft_print_msg("Link on %s is not up at step %u: %d (%s)\n",
+			       ifobj->ifname, test->current_step, err, strerror(-err));
+		xsk_peer_abort(ctrl_peer);
+	}
+	return err;
+}
+
 static int sync_endpoints(struct test_spec *test)
 {
 	int err = xsk_peer_ready(ctrl_peer);
 
-	if (err)
+	if (err && xsk_peer_skipped(ctrl_peer)) {
+		ksft_print_msg("The peer endpoint skipped the case at step %u\n",
+			       test->current_step);
+		return TEST_SKIP;
+	}
+	if (err) {
 		ksft_print_msg("Endpoint synchronization failed at step %u: %d (%s)\n",
 			       test->current_step, err, strerror(-err));
-	return err;
+		return err;
+	}
+	return wait_link_up(test);
 }
 
 int worker_testapp_validate_tx(struct test_spec *test)
@@ -1810,7 +1874,7 @@ int worker_testapp_validate_tx(struct test_spec *test)
 		return err;
 
 	err = testapp_validate_tx_endpoint(test, test->ifobj_tx);
-	if (err) {
+	if (err && err != TEST_SKIP) {
 		ksft_print_msg("TX endpoint traffic failed at step %u: %d\n",
 			       test->current_step, err);
 		xsk_peer_abort(ctrl_peer);
@@ -1905,6 +1969,10 @@ static int xsk_attach_xdp_progs(struct test_spec *test, struct ifobject *ifobj_r
 	if (!ifobj_tx || !ifobj_is_local(ifobj_tx))
 		return 0;
 
+	/* A ZC driver can require XDP to be enabled for TX wakeups. */
+	if (!ifobj_tx->rx_on && test->mode != TEST_MODE_ZC)
+		return 0;
+
 	if (xdp_prog_changed_tx(test))
 		err = xsk_reattach_xdp(ifobj_tx, test->xdp_prog_tx, test->xskmap_tx, test->mode);
 
@@ -1986,6 +2054,8 @@ static int __testapp_validate_traffic(struct test_spec *test, struct ifobject *i
 		clean_umem(test, ifobj1, ifobj2);
 	}
 
+	if (err == TEST_SKIP)
+		return TEST_SKIP;
 	return err ? TEST_FAILURE : TEST_PASS;
 }
 
@@ -2012,11 +2082,12 @@ static int testapp_validate_traffic_single_thread(struct test_spec *test, struct
 
 int testapp_teardown(struct test_spec *test)
 {
-	int i;
+	int i, ret;
 
 	for (i = 0; i < MAX_TEARDOWN_ITER; i++) {
-		if (testapp_validate_traffic(test))
-			return TEST_FAILURE;
+		ret = testapp_validate_traffic(test);
+		if (ret)
+			return ret;
 		test_spec_reset(test);
 	}
 
@@ -2042,8 +2113,9 @@ int testapp_bidirectional(struct test_spec *test)
 	test->ifobj_tx->rx_on = true;
 	test->ifobj_rx->tx_on = true;
 	test->total_steps = 2;
-	if (testapp_validate_traffic(test))
-		return TEST_FAILURE;
+	res = testapp_validate_traffic(test);
+	if (res)
+		return res;
 
 	print_verbose("Switching Tx/Rx direction\n");
 	swap_directions(&test->ifobj_rx, &test->ifobj_tx);
@@ -2076,10 +2148,13 @@ static int swap_xsk_resources(struct test_spec *test)
 
 int testapp_xdp_prog_cleanup(struct test_spec *test)
 {
+	int ret;
+
 	test->total_steps = 2;
 	test->nb_sockets = 2;
-	if (testapp_validate_traffic(test))
-		return TEST_FAILURE;
+	ret = testapp_validate_traffic(test);
+	if (ret)
+		return ret;
 
 	if (swap_xsk_resources(test)) {
 		clean_sockets(test, test->ifobj_rx);
@@ -2361,7 +2436,9 @@ int testapp_too_many_frags(struct test_spec *test)
 	u32 max_frags, i;
 	int ret = TEST_FAILURE;
 
-	if (test->mode == TEST_MODE_ZC) {
+	if (max_frags_override) {
+		max_frags = max_frags_override;
+	} else if (test->mode == TEST_MODE_ZC) {
 		max_frags = xsk_get_cap(test, xdp_zc_max_segs);
 	} else {
 		max_frags = xsk_get_cap(test, max_skb_frags);
@@ -2457,6 +2534,7 @@ static int detect_ifobj_caps(struct ifobject *ifobj)
 	if (query_opts.feature_flags & NETDEV_XDP_ACT_RX_SG)
 		ifobj_set_cap(ifobj, XSK_CAP_MBUF);
 	if (query_opts.feature_flags & NETDEV_XDP_ACT_XSK_ZEROCOPY) {
+		ifobj_set_cap(ifobj, XSK_CAP_ZC_ADVERTISED);
 		if (query_opts.xdp_zc_max_segs > 1) {
 			ifobj_set_cap(ifobj, XSK_CAP_MBUF_ZC);
 			ifobj->caps.xdp_zc_max_segs = query_opts.xdp_zc_max_segs;
@@ -2700,9 +2778,18 @@ int testapp_adjust_tail_shrink_mb(struct test_spec *test)
 	return testapp_adjust_tail(test, -XSK_UMEM__MAX_FRAME_SIZE, XSK_UMEM__LARGE_FRAME_SIZE * 2);
 }
 
+/* Generic XDP leaves no room to grow the tail. Only the RX endpoint runs the
+ * program on the test flow, and the TX endpoint, which does not know the mode
+ * of its peer, learns about the skip from it.
+ */
+static bool adjust_tail_grow_skip(struct test_spec *test)
+{
+	return ifobj_is_local(test->ifobj_rx) && test->mode == TEST_MODE_SKB;
+}
+
 int testapp_adjust_tail_grow(struct test_spec *test)
 {
-	if (test->mode == TEST_MODE_SKB)
+	if (adjust_tail_grow_skip(test))
 		return TEST_SKIP;
 
 	/* Grow by 4 bytes for testing purpose */
@@ -2713,7 +2800,7 @@ int testapp_adjust_tail_grow_mb(struct test_spec *test)
 {
 	u32 grow_size;
 
-	if (test->mode == TEST_MODE_SKB)
+	if (adjust_tail_grow_skip(test))
 		return TEST_SKIP;
 
 	/* worst case scenario is when underlying setup will work on 3k
diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index 4690ae2ad9c6..15534f4ec8f4 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -86,6 +86,8 @@ struct xsk_peer;
 /* The control channel to the other endpoint. */
 void xsk_set_endpoint(struct xsk_peer *peer);
 int xsk_set_flow(const char *src_ip, const char *dst_ip, u16 port);
+void xsk_set_max_frags(u32 max_frags);
+void xsk_set_timeout(u32 timeout);
 
 struct xsk_socket_info {
 	struct xsk_ring_cons rx;
@@ -126,6 +128,7 @@ struct xsk_umem_info {
 #define XSK_CAP_HW_RING          (1U << 3)
 #define XSK_CAP_DRV              (1U << 4)
 #define XSK_CAP_ZC               (1U << 5)
+#define XSK_CAP_ZC_ADVERTISED    (1U << 6)
 
 struct xsk_caps {
 	u32 flags;
@@ -149,7 +152,11 @@ struct ifobject {
 	struct ethtool_ringparam ring;
 	enum test_mode mode;
 	int ifindex;
+	u32 queue_id;
 	int mtu;
+	/* Device MTU, which an SKB-mode endpoint may keep above mtu. */
+	int dev_mtu;
+	int orig_mtu;
 	u32 bind_flags;
 	bool tx_on;
 	bool rx_on;
diff --git a/tools/testing/selftests/net/lib/xsk/xsk.c b/tools/testing/selftests/net/lib/xsk/xsk.c
index 54e6808e3fe4..6249a4955b64 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk.c
+++ b/tools/testing/selftests/net/lib/xsk/xsk.c
@@ -828,3 +828,37 @@ int xsk_get_mac(const char *ifname, u8 mac[ETH_ALEN])
 		memcpy(mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
 	return err;
 }
+
+int xsk_get_mtu(const char *ifname, int *mtu)
+{
+	struct ifreq ifr = {};
+	int err;
+
+	err = xsk_ifreq_ioctl(ifname, SIOCGIFMTU, &ifr);
+	if (!err)
+		*mtu = ifr.ifr_mtu;
+	return err;
+}
+
+/* ETHTOOL_GLINK reports the carrier. SIOCGIFFLAGS cannot: IFF_LOWER_UP does
+ * not fit in ifr_flags, and IFF_RUNNING follows the carrier only once the link
+ * watch runs. A driver without .get_link has nothing to wait for.
+ */
+int xsk_wait_link_up(const char *ifname, int timeout_ms)
+{
+	struct ethtool_value edata = { .cmd = ETHTOOL_GLINK };
+	struct ifreq ifr = { .ifr_data = (void *)&edata };
+	int err;
+
+	for (;;) {
+		err = xsk_ifreq_ioctl(ifname, SIOCETHTOOL, &ifr);
+		if (err == -EOPNOTSUPP)
+			return 0;
+		if (err || edata.data)
+			return err;
+		if (timeout_ms <= 0)
+			return -ETIMEDOUT;
+		usleep(100 * 1000);
+		timeout_ms -= 100;
+	}
+}
diff --git a/tools/testing/selftests/net/lib/xsk/xsk.h b/tools/testing/selftests/net/lib/xsk/xsk.h
index 263971a57e1e..181c706cb418 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/xsk.h
@@ -245,6 +245,8 @@ int xsk_set_mtu(int ifindex, int mtu);
 
 int get_hw_ring_size(char *ifname, struct ethtool_ringparam *ring_param);
 int xsk_get_mac(const char *ifname, u8 mac[ETH_ALEN]);
+int xsk_get_mtu(const char *ifname, int *mtu);
+int xsk_wait_link_up(const char *ifname, int timeout_ms);
 
 #ifdef __cplusplus
 } /* extern "C" */
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_peer.c b/tools/testing/selftests/net/lib/xsk/xsk_peer.c
index e3e353d6abd9..256e63196f92 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk_peer.c
+++ b/tools/testing/selftests/net/lib/xsk/xsk_peer.c
@@ -24,6 +24,7 @@ enum xsk_peer_msg_type {
 	XSK_PEER_READY = 1,
 	XSK_PEER_PROGRESS,
 	XSK_PEER_ABORT,
+	XSK_PEER_SKIP,
 };
 
 struct xsk_peer_msg {
@@ -37,6 +38,7 @@ struct xsk_peer {
 	u32 acked;
 	bool ready_seen;
 	bool closed;
+	bool skipped;
 };
 
 static int io_full(int fd, void *buf, size_t len, bool write_op)
@@ -110,6 +112,11 @@ static int peer_recv_one(struct xsk_peer *peer)
 		return 0;
 	case XSK_PEER_ABORT:
 		return -ECANCELED;
+	case XSK_PEER_SKIP:
+		/* The peer skips the case and closes the connection. */
+		peer->closed = true;
+		peer->skipped = true;
+		return -EOPNOTSUPP;
 	default:
 		return -EPROTO;
 	}
@@ -145,6 +152,16 @@ int xsk_peer_ready(struct xsk_peer *peer)
 	int err;
 
 	err = peer_send(peer, XSK_PEER_READY, 0);
+	/*
+	 * Read what a gone peer left, such as a SKIP, up to the end, even if its
+	 * READY came early.
+	 */
+	if (peer_gone(err)) {
+		do {
+			err = peer_recv_one(peer);
+		} while (!err);
+		return err;
+	}
 	while (!err && !peer->ready_seen)
 		err = peer_recv_one(peer);
 	if (err)
@@ -162,6 +179,30 @@ void xsk_peer_abort(struct xsk_peer *peer)
 		peer_send(peer, XSK_PEER_ABORT, 0);
 }
 
+/*
+ * Tell the peer that this endpoint skips the case, so that it skips too.
+ * Closing with unread data, or getting data once closed, resets the
+ * connection and drops the SKIP if the peer has yet to acknowledge it, as
+ * when a NIC reset holds the link down. So read until the peer closes.
+ */
+void xsk_peer_skip(struct xsk_peer *peer, int timeout_ms)
+{
+	struct pollfd pfd = { .events = POLLIN };
+	struct xsk_peer_msg msg;
+	int ret;
+
+	if (!peer || peer->closed || peer_send(peer, XSK_PEER_SKIP, 0))
+		return;
+
+	shutdown(peer->fd, SHUT_WR);
+	pfd.fd = peer->fd;
+	do {
+		ret = poll(&pfd, 1, timeout_ms);
+		if (ret > 0)
+			ret = read(peer->fd, &msg, sizeof(msg));
+	} while (ret > 0 || (ret < 0 && errno == EINTR));
+}
+
 int xsk_peer_rx_progress(struct xsk_peer *peer, u32 pkts)
 {
 	int err;
@@ -195,6 +236,11 @@ bool xsk_peer_closed(struct xsk_peer *peer)
 	return peer->closed;
 }
 
+bool xsk_peer_skipped(struct xsk_peer *peer)
+{
+	return peer->skipped;
+}
+
 static int accept_tmout(int lfd)
 {
 	struct pollfd pfd = { .fd = lfd, .events = POLLIN };
diff --git a/tools/testing/selftests/net/lib/xsk/xsk_peer.h b/tools/testing/selftests/net/lib/xsk/xsk_peer.h
index da4462192994..26ae6b1bb384 100644
--- a/tools/testing/selftests/net/lib/xsk/xsk_peer.h
+++ b/tools/testing/selftests/net/lib/xsk/xsk_peer.h
@@ -12,8 +12,10 @@ void xsk_peer_close(struct xsk_peer *peer);
 
 int xsk_peer_ready(struct xsk_peer *peer);
 void xsk_peer_abort(struct xsk_peer *peer);
+void xsk_peer_skip(struct xsk_peer *peer, int timeout_ms);
 int xsk_peer_rx_progress(struct xsk_peer *peer, u32 pkts);
 int xsk_peer_tx_sync(struct xsk_peer *peer);
 bool xsk_peer_closed(struct xsk_peer *peer);
+bool xsk_peer_skipped(struct xsk_peer *peer);
 
 #endif /* XSK_PEER_H_ */
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index 9704702f60fc..56ea5ac6a3c6 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -119,6 +119,9 @@ static const char *opt_dst_ip;
 static const struct ether_addr *opt_dst_mac;
 static struct ether_addr dst_mac;
 static u16 opt_udp_port;
+static u32 opt_queue;
+static u32 opt_max_frags;
+static u32 opt_timeout = 3;
 
 enum {
 	OPT_LISTEN = 256,
@@ -126,6 +129,9 @@ enum {
 	OPT_DST_IP,
 	OPT_DST_MAC,
 	OPT_UDP_PORT,
+	OPT_QUEUE,
+	OPT_MAX_FRAGS,
+	OPT_TIMEOUT,
 };
 
 static void __exit_with_error(int error, const char *file, const char *func, int line)
@@ -137,49 +143,6 @@ static void __exit_with_error(int error, const char *file, const char *func, int
 
 #define exit_with_error(error) __exit_with_error(error, __FILE__, __func__, __LINE__)
 
-static bool ifobj_zc_avail(struct ifobject *ifobject)
-{
-	size_t umem_sz = DEFAULT_UMEM_BUFFERS * XSK_UMEM__DEFAULT_FRAME_SIZE;
-	int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE;
-	struct xsk_socket_info *xsk;
-	struct xsk_umem_info *umem;
-	bool zc_avail = false;
-	void *bufs;
-	int ret;
-
-	bufs = mmap(NULL, umem_sz, PROT_READ | PROT_WRITE, mmap_flags, -1, 0);
-	if (bufs == MAP_FAILED)
-		exit_with_error(errno);
-
-	umem = calloc(1, sizeof(struct xsk_umem_info));
-	if (!umem) {
-		munmap(bufs, umem_sz);
-		exit_with_error(ENOMEM);
-	}
-	umem->frame_size = XSK_UMEM__DEFAULT_FRAME_SIZE;
-	ret = xsk_configure_umem(ifobject, umem, bufs, umem_sz);
-	if (ret)
-		exit_with_error(-ret);
-
-	xsk = calloc(1, sizeof(struct xsk_socket_info));
-	if (!xsk)
-		goto out;
-	ifobject->bind_flags = XDP_USE_NEED_WAKEUP | XDP_ZEROCOPY;
-	ifobject->rx_on = true;
-	xsk->rxqsize = XSK_RING_CONS__DEFAULT_NUM_DESCS;
-	ret = xsk_configure_socket(xsk, umem, ifobject, false);
-	if (!ret)
-		zc_avail = true;
-
-	xsk_socket__delete(xsk->xsk);
-	free(xsk);
-out:
-	munmap(umem->buffer, umem_sz);
-	xsk_umem__delete(umem->umem);
-	free(umem);
-	return zc_avail;
-}
-
 static struct option long_options[] = {
 	{"interface", required_argument, 0, 'i'},
 	{"busy-poll", no_argument, 0, 'b'},
@@ -195,6 +158,9 @@ static struct option long_options[] = {
 	{"dst-ip", required_argument, 0, OPT_DST_IP},
 	{"dst-mac", required_argument, 0, OPT_DST_MAC},
 	{"udp-port", required_argument, 0, OPT_UDP_PORT},
+	{"queue", required_argument, 0, OPT_QUEUE},
+	{"max-frags", required_argument, 0, OPT_MAX_FRAGS},
+	{"timeout", required_argument, 0, OPT_TIMEOUT},
 	{"help", no_argument, 0, 'h'},
 	{0, 0, 0, 0}
 };
@@ -219,6 +185,9 @@ static void print_usage(char **argv)
 		"      --dst-ip IP      Destination address, of the same family\n"
 		"      --dst-mac MAC    Destination MAC address of the test packets\n"
 		"      --udp-port PORT  UDP destination port, and source port of socket 0\n"
+		"      --queue N        AF_XDP queue to bind (default 0)\n"
+		"      --max-frags N    Fragment limit for TOO_MANY_FRAGS (default by mode)\n"
+		"      --timeout SEC    Traffic and link timeout (default 3)\n"
 		"  -h, --help           Display this help and exit\n";
 
 	ksft_print_msg(str, basename(argv[0]));
@@ -243,6 +212,11 @@ static void bind_iface(struct ifobject *ifobj, const char *ifname, char **argv)
 		ksft_print_msg("Error: cannot initialize interface %s\n", ifobj->ifname);
 		ksft_exit_fail();
 	}
+	if (xsk_get_mtu(ifobj->ifname, &ifobj->dev_mtu)) {
+		ksft_print_msg("Error: cannot read MTU of interface %s\n", ifobj->ifname);
+		ksft_exit_fail();
+	}
+	ifobj->orig_mtu = ifobj->dev_mtu;
 }
 
 static void print_tests(void)
@@ -328,6 +302,18 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 		case OPT_UDP_PORT:
 			opt_udp_port = strtoul(optarg, NULL, 10);
 			break;
+		case OPT_QUEUE:
+			opt_queue = strtoul(optarg, NULL, 10);
+			break;
+		case OPT_MAX_FRAGS:
+			/* It sizes the packet array of TOO_MANY_FRAGS. */
+			opt_max_frags = strtoul(optarg, NULL, 10);
+			if (opt_max_frags > 0xffff)
+				print_usage(argv);
+			break;
+		case OPT_TIMEOUT:
+			opt_timeout = strtoul(optarg, NULL, 10);
+			break;
 		case 'h':
 		default:
 			print_usage(argv);
@@ -351,7 +337,12 @@ static int run_pkt_test(struct test_spec *test)
 	int ret;
 
 	ret = test->test_func(test);
+	pkt_stream_restore_default(test);
+	return ret;
+}
 
+static void report_pkt_test(struct test_spec *test, int ret)
+{
 	switch (ret) {
 	case TEST_PASS:
 		ksft_test_result_pass("PASS: %s %s%s\n", mode_string(test), busy_poll_string(test),
@@ -369,9 +360,6 @@ static int run_pkt_test(struct test_spec *test)
 		ksft_test_result_fail("FAIL: %s %s%s -- Unexpected returned value (%d)\n",
 				      mode_string(test), busy_poll_string(test), test->name, ret);
 	}
-
-	pkt_stream_restore_default(test);
-	return ret;
 }
 
 static bool is_xdp_supported(int ifindex)
@@ -402,12 +390,14 @@ static bool is_xdp_supported(int ifindex)
 	return true;
 }
 
+/* Probe only the mode that the endpoint runs in. */
 static u32 detect_mode_caps(struct ifobject *ifobj)
 {
-	if (is_xdp_supported(ifobj->ifindex)) {
+	if (opt_mode == TEST_MODE_ZC && ifobj_has_cap(ifobj, XSK_CAP_ZC_ADVERTISED)) {
+		/* Trust the advertised flag; an XDP_ZEROCOPY bind never falls back to copy. */
+		ifobj_set_cap(ifobj, XSK_CAP_DRV | XSK_CAP_ZC);
+	} else if (opt_mode == TEST_MODE_DRV && is_xdp_supported(ifobj->ifindex)) {
 		ifobj_set_cap(ifobj, XSK_CAP_DRV);
-		if (ifobj_zc_avail(ifobj))
-			ifobj_set_cap(ifobj, XSK_CAP_ZC);
 	}
 
 	return ifobj->caps.flags;
@@ -422,8 +412,10 @@ static bool mode_supported(enum test_mode mode, u32 caps)
 	return caps & XSK_CAP_ZC;
 }
 
-static void cleanup_iface(struct ifobject *ifobj)
+static int cleanup_iface(struct ifobject *ifobj)
 {
+	int err = 0;
+
 	/* A peer shadow has a skeleton but no bound interface. */
 	if (!ifobj_is_local(ifobj))
 		goto unload;
@@ -431,8 +423,16 @@ static void cleanup_iface(struct ifobject *ifobj)
 	if (ifobj->xdp_prog)
 		xsk_detach_xdp_program(ifobj->ifindex,
 				       mode_to_xdp_flags(ifobj->mode));
+	/* After the detach, as an attached program can cap the MTU. */
+	if (ifobj->dev_mtu != ifobj->orig_mtu) {
+		err = xsk_set_mtu(ifobj->ifindex, ifobj->orig_mtu);
+		if (err)
+			ksft_print_msg("Failed to restore MTU %d on %s\n",
+				       ifobj->orig_mtu, ifobj->ifname);
+	}
 unload:
 	xsk_unload_xdp_programs(ifobj);
+	return err;
 }
 
 /* Connect the two endpoints without negotiating the remote NIC's capabilities. */
@@ -469,6 +469,7 @@ int main(int argc, char **argv)
 	struct test_spec test = {};
 	int ret = TEST_FAILURE;
 	u32 caps;
+	int err;
 
 	/* Use libbpf 1.0 API mode */
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
@@ -512,6 +513,8 @@ int main(int argc, char **argv)
 		ksft_exit_xfail();
 	}
 
+	xsk_set_max_frags(opt_max_frags);
+	xsk_set_timeout(opt_timeout);
 	/* Before the default packet streams, which have its minimum size. */
 	if (xsk_set_flow(opt_src_ip, opt_dst_ip, opt_udp_port))
 		print_usage(argv);
@@ -527,6 +530,7 @@ int main(int argc, char **argv)
 		shadow_ifobj = ifobj_tx;
 	}
 	bind_iface(local_ifobj, opt_ifname, argv);
+	local_ifobj->queue_id = opt_queue;
 	caps = detect_mode_caps(local_ifobj);
 
 	test.tx_pkt_stream_default = pkt_stream_generate(DEFAULT_PKT_CNT, MIN_PKT_SIZE);
@@ -552,8 +556,17 @@ int main(int argc, char **argv)
 
 out:
 	xsk_set_endpoint(NULL);
-	cleanup_iface(ifobj_tx);
-	cleanup_iface(ifobj_rx);
+	err = cleanup_iface(ifobj_tx);
+	err |= cleanup_iface(ifobj_rx);
+	/* xsk.py trusts a passed or skipped endpoint to have undone the MTU. */
+	if (err)
+		ret = TEST_FAILURE;
+	/* After the cleanup, which can still fail a case that ran. */
+	if (test.test_func)
+		report_pkt_test(&test, ret);
+	/* The peer skips the case too, rather than fail on the closed connection. */
+	if (ret == TEST_SKIP)
+		xsk_peer_skip(peer, opt_timeout * 1000);
 	xsk_peer_close(peer);
 	pkt_stream_delete(test.tx_pkt_stream_default);
 	pkt_stream_delete(test.rx_pkt_stream_default);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 13/15] selftests: xsk: list the hardware test cases
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (11 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 12/15] selftests: xsk: prepare xskxceiver for endpoints on two hosts Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 14/15] selftests: drv-net: test AF_XDP zero-copy with an SKB peer Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 15/15] selftests: xsk: document generic and hardware endpoint runs Maciej Fijalkowski
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

The hardware test that the next patch adds runs each case with its
zero-copy endpoint as RX, as TX or as both. A list of the cases by ID
in that runner would repeat the test table and silently go stale when a
case is added or moved.

Split tests[] into the tests that check both endpoint roles, those that
check the RX endpoint, those that check the TX endpoint, and the
generic ones, which a hardware run does not do: BIDIRECTIONAL swaps the
directions, and STAT_RX_DROPPED and TX_QUEUE_CONSUMER skip in zero-copy
mode. The test IDs follow the order of these arrays, so they change,
and the runners take them from xskxceiver. The notes that came with
ci_skip_tests[] go.

-L lists each hardware test once per role that it tests, as
"ID: NAME rx" or "ID: NAME tx", while -l still lists every test by ID.

Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/test_xsk.h  | 68 +++++++++++++------
 .../selftests/net/lib/xsk/xskxceiver.c        | 47 ++++++++++---
 2 files changed, 84 insertions(+), 31 deletions(-)

diff --git a/tools/testing/selftests/net/lib/xsk/test_xsk.h b/tools/testing/selftests/net/lib/xsk/test_xsk.h
index 15534f4ec8f4..a59b34b156d7 100644
--- a/tools/testing/selftests/net/lib/xsk/test_xsk.h
+++ b/tools/testing/selftests/net/lib/xsk/test_xsk.h
@@ -304,49 +304,73 @@ int testapp_xdp_shared_umem(struct test_spec *test);
 int worker_testapp_validate_rx(struct test_spec *test);
 int worker_testapp_validate_tx(struct test_spec *test);
 
-static const struct test_spec tests[] = {
+/* Tests by the role of the endpoint that they check. A hardware run, see
+ * xskxceiver -L, gives that role to its zero-copy endpoint. It does not do
+ * the generic tests: BIDIRECTIONAL swaps the directions, which
+ * drivers/net/hw/xsk.py sets up one at a time, and the others skip in
+ * zero-copy mode.
+ */
+static const struct test_spec hw_rx_tx_tests[] = {
 	{.name = "SEND_RECEIVE", .test_func = testapp_send_receive},
 	{.name = "SEND_RECEIVE_2K_FRAME", .test_func = testapp_send_receive_2k_frame},
 	{.name = "SEND_RECEIVE_SINGLE_PKT", .test_func = testapp_single_pkt},
+	{.name = "SEND_RECEIVE_9K_PACKETS", .test_func = testapp_send_receive_mb},
+	{.name = "SEND_RECEIVE_UNALIGNED", .test_func = testapp_send_receive_unaligned},
+	{.name = "SEND_RECEIVE_UNALIGNED_9K_PACKETS",
+	 .test_func = testapp_send_receive_unaligned_mb},
+	{.name = "HW_SW_MAX_RING_SIZE", .test_func = testapp_hw_sw_max_ring_size},
+	{.name = "TEARDOWN", .test_func = testapp_teardown},
+	{.name = "XDP_SHARED_UMEM", .test_func = testapp_xdp_shared_umem},
+};
+
+static const struct test_spec hw_rx_tests[] = {
 	{.name = "POLL_RX", .test_func = testapp_poll_rx},
-	{.name = "POLL_TX", .test_func = testapp_poll_tx},
 	{.name = "POLL_RXQ_FULL", .test_func = testapp_poll_rxq_tmout},
-	{.name = "POLL_TXQ_FULL", .test_func = testapp_poll_txq_tmout},
-	{.name = "ALIGNED_INV_DESC", .test_func = testapp_aligned_inv_desc},
-	{.name = "ALIGNED_INV_DESC_2K_FRAME_SIZE", .test_func = testapp_aligned_inv_desc_2k_frame},
 	{.name = "UMEM_HEADROOM", .test_func = testapp_headroom},
-	{.name = "BIDIRECTIONAL", .test_func = testapp_bidirectional},
-	{.name = "STAT_RX_DROPPED", .test_func = testapp_stats_rx_dropped},
-	{.name = "STAT_TX_INVALID", .test_func = testapp_stats_tx_invalid_descs},
 	{.name = "STAT_RX_FULL", .test_func = testapp_stats_rx_full},
 	{.name = "STAT_FILL_EMPTY", .test_func = testapp_stats_fill_empty},
 	{.name = "XDP_PROG_CLEANUP", .test_func = testapp_xdp_prog_cleanup},
 	{.name = "XDP_DROP_HALF", .test_func = testapp_xdp_drop},
-	{.name = "XDP_SHARED_UMEM", .test_func = testapp_xdp_shared_umem},
 	{.name = "XDP_METADATA_COPY", .test_func = testapp_xdp_metadata},
 	{.name = "XDP_METADATA_COPY_MULTI_BUFF", .test_func = testapp_xdp_metadata_mb},
-	{.name = "ALIGNED_INV_DESC_MULTI_BUFF", .test_func = testapp_aligned_inv_desc_mb},
-	{.name = "TOO_MANY_FRAGS", .test_func = testapp_too_many_frags},
 	{.name = "XDP_ADJUST_TAIL_SHRINK", .test_func = testapp_adjust_tail_shrink},
-	{.name = "TX_QUEUE_CONSUMER", .test_func = testapp_tx_queue_consumer},
-	/* Flaky tests */
 	{.name = "XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF", .test_func = testapp_adjust_tail_shrink_mb},
 	{.name = "XDP_ADJUST_TAIL_GROW", .test_func = testapp_adjust_tail_grow},
 	{.name = "XDP_ADJUST_TAIL_GROW_MULTI_BUFF", .test_func = testapp_adjust_tail_grow_mb},
-	{.name = "SEND_RECEIVE_9K_PACKETS", .test_func = testapp_send_receive_mb},
-	/* Tests with huge page dependency */
-	{.name = "SEND_RECEIVE_UNALIGNED", .test_func = testapp_send_receive_unaligned},
+};
+
+static const struct test_spec hw_tx_tests[] = {
+	{.name = "POLL_TX", .test_func = testapp_poll_tx},
+	{.name = "POLL_TXQ_FULL", .test_func = testapp_poll_txq_tmout},
+	{.name = "ALIGNED_INV_DESC", .test_func = testapp_aligned_inv_desc},
+	{.name = "ALIGNED_INV_DESC_2K_FRAME_SIZE", .test_func = testapp_aligned_inv_desc_2k_frame},
+	{.name = "STAT_TX_INVALID", .test_func = testapp_stats_tx_invalid_descs},
+	{.name = "ALIGNED_INV_DESC_MULTI_BUFF", .test_func = testapp_aligned_inv_desc_mb},
+	{.name = "TOO_MANY_FRAGS", .test_func = testapp_too_many_frags},
 	{.name = "UNALIGNED_INV_DESC", .test_func = testapp_unaligned_inv_desc},
 	{.name = "UNALIGNED_INV_DESC_4001_FRAME_SIZE",
 	 .test_func = testapp_unaligned_inv_desc_4001_frame},
-	{.name = "SEND_RECEIVE_UNALIGNED_9K_PACKETS",
-	 .test_func = testapp_send_receive_unaligned_mb},
 	{.name = "UNALIGNED_INV_DESC_MULTI_BUFF", .test_func = testapp_unaligned_inv_desc_mb},
-	/* Test with HW ring size dependency */
 	{.name = "HW_SW_MIN_RING_SIZE", .test_func = testapp_hw_sw_min_ring_size},
-	{.name = "HW_SW_MAX_RING_SIZE", .test_func = testapp_hw_sw_max_ring_size},
-	/* Too long test */
-	{.name = "TEARDOWN", .test_func = testapp_teardown},
+};
+
+static const struct test_spec generic_tests[] = {
+	{.name = "BIDIRECTIONAL", .test_func = testapp_bidirectional},
+	{.name = "STAT_RX_DROPPED", .test_func = testapp_stats_rx_dropped},
+	{.name = "TX_QUEUE_CONSUMER", .test_func = testapp_tx_queue_consumer},
+};
+
+/* A test ID is the index of the test in these arrays taken in order. */
+static const struct test_group {
+	const struct test_spec *tests;
+	u32 nb_tests;
+	bool hw_rx;
+	bool hw_tx;
+} test_groups[] = {
+	{hw_rx_tx_tests, ARRAY_SIZE(hw_rx_tx_tests), true, true},
+	{hw_rx_tests, ARRAY_SIZE(hw_rx_tests), true, false},
+	{hw_tx_tests, ARRAY_SIZE(hw_tx_tests), false, true},
+	{generic_tests, ARRAY_SIZE(generic_tests), false, false},
 };
 
 
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index 56ea5ac6a3c6..c52e3e376bc5 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -149,6 +149,7 @@ static struct option long_options[] = {
 	{"verbose", no_argument, 0, 'v'},
 	{"mode", required_argument, 0, 'm'},
 	{"list", no_argument, 0, 'l'},
+	{"list-hw", no_argument, 0, 'L'},
 	{"test", required_argument, 0, 't'},
 	{"endpoint", required_argument, 0, 'e'},
 	{"peer", required_argument, 0, 'p'},
@@ -176,6 +177,7 @@ static void print_usage(char **argv)
 		"  -b, --busy-poll      Enable busy poll\n"
 		"  -m, --mode           Mode to run: skb, drv or zc\n"
 		"  -l, --list           List all available tests\n"
+		"  -L, --list-hw        List hardware tests and the roles they test\n"
 		"  -t, --test           Test case to run, a number from -l\n"
 		"  -e, --endpoint       Role of this endpoint: rx or tx\n"
 		"  -p, --peer           Control host: IPv4/IPv6 address or hostname\n"
@@ -219,13 +221,38 @@ static void bind_iface(struct ifobject *ifobj, const char *ifname, char **argv)
 	ifobj->orig_mtu = ifobj->dev_mtu;
 }
 
-static void print_tests(void)
+/* List each test, or each hardware test once per role it tests. */
+static void print_tests(bool hw)
+{
+	const struct test_group *group;
+	u32 i, j, id = 0;
+
+	printf("%s:\n", hw ? "Hardware tests" : "Tests");
+	for (i = 0; i < ARRAY_SIZE(test_groups); i++) {
+		group = &test_groups[i];
+		for (j = 0; j < group->nb_tests; j++, id++) {
+			const char *name = group->tests[j].name;
+
+			if (!hw)
+				printf("%u: %s\n", id, name);
+			if (hw && group->hw_rx)
+				printf("%u: %s rx\n", id, name);
+			if (hw && group->hw_tx)
+				printf("%u: %s tx\n", id, name);
+		}
+	}
+}
+
+static const struct test_spec *find_test(u32 id)
 {
 	u32 i;
 
-	printf("Tests:\n");
-	for (i = 0; i < ARRAY_SIZE(tests); i++)
-		printf("%u: %s\n", i, tests[i].name);
+	for (i = 0; i < ARRAY_SIZE(test_groups); i++) {
+		if (id < test_groups[i].nb_tests)
+			return &test_groups[i].tests[id];
+		id -= test_groups[i].nb_tests;
+	}
+	return NULL;
 }
 
 static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj_rx, int argc,
@@ -236,7 +263,7 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 	opterr = 0;
 
 	for (;;) {
-		c = getopt_long(argc, argv, "i:vbm:lt:e:p:P:", long_options, &option_index);
+		c = getopt_long(argc, argv, "i:vbm:lLt:e:p:P:", long_options, &option_index);
 		if (c == -1)
 			break;
 
@@ -262,7 +289,8 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
 				print_usage(argv);
 			break;
 		case 'l':
-			print_tests();
+		case 'L':
+			print_tests(c == 'L');
 			ksft_exit_pass();
 			break;
 		case 't':
@@ -461,8 +489,8 @@ static int setup_peer(struct ifobject *local, struct ifobject *shadow,
 int main(int argc, char **argv)
 {
 	u32 cache_line_size, max_frags, umem_tailroom;
-	const size_t total_tests = ARRAY_SIZE(tests);
 	struct ifobject *ifobj_tx, *ifobj_rx;
+	const struct test_spec *test_to_run;
 	struct ifobject *shadow_ifobj;
 	struct ifobject *local_ifobj;
 	struct xsk_peer *peer = NULL;
@@ -508,7 +536,8 @@ int main(int argc, char **argv)
 	ifobj_tx->caps.umem_tailroom = umem_tailroom;
 	ifobj_rx->caps.umem_tailroom = umem_tailroom;
 
-	if (opt_run_test != RUN_ALL_TESTS && opt_run_test >= total_tests) {
+	test_to_run = find_test(opt_run_test);
+	if (!test_to_run) {
 		ksft_print_msg("Error: test %u does not exist.\n", opt_run_test);
 		ksft_exit_xfail();
 	}
@@ -551,7 +580,7 @@ int main(int argc, char **argv)
 	}
 
 	ksft_set_plan(1);
-	test_init(&test, ifobj_tx, ifobj_rx, opt_mode, &tests[opt_run_test]);
+	test_init(&test, ifobj_tx, ifobj_rx, opt_mode, test_to_run);
 	ret = run_pkt_test(&test);
 
 out:
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 14/15] selftests: drv-net: test AF_XDP zero-copy with an SKB peer
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (12 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 13/15] selftests: xsk: list the hardware test cases Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  2026-10-16 17:43 ` [PATCH v4 net-next 15/15] selftests: xsk: document generic and hardware endpoint runs Maciej Fijalkowski
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Run the AF_XDP cases that a physical link can carry as pairs of
xskxceiver processes: a zero-copy endpoint on the DUT and an SKB-mode
endpoint on the remote, which needs no zero-copy support. xsk.py runs
the cases that xskxceiver -L lists, in the DUT directions listed with
them, as KTAP variants named <rx|tx>_<CASE>. test_xsk runs them with
the DUT socket driven by interrupts and test_xsk_busy_poll with it busy
polling, after xsk_setup_busy_poll() sets napi_defer_hard_irqs and
gro_flush_timeout on the DUT so that SO_PREFER_BUSY_POLL keeps its IRQs
masked, as on veth.

The endpoints send UDP over IPv6 if LOCAL_V6 and REMOTE_V6 are set, and
over IPv4 otherwise, each to the MAC address of its next hop to the
other: the peer's if the peer's address is on link, else the gateway's.

main() sets up what the cases share once, as defer() works only within
a case, and an ExitStack undoes it however the run ends, -l and -h
included, with each step registered as soon as it is done:

- the DUT endpoint binds the last queue with both an RX and a TX ring,
  which is taken out of RSS with ethtool -X equal; a TX endpoint fills
  no RX buffers, so in zero-copy mode its queue drops what it receives
  and TX cases need the queue out of RSS as much as RX cases do. A DUT
  with fewer than two such queues, or that cannot take one out of RSS,
  skips. ethtool -X default then hands the indirection table back to
  the driver, and the saved table is written back if it was not the
  default
- an ntuple rule on each host steers the test UDP flow to the receiving
  endpoint: the DUT queue in RX cases and queue 0 of the remote, where
  its endpoint binds, in TX cases. A host without ntuple support, or
  that rejects the rule, skips. Setting the rules once spares each TX
  case the ssh round trips of a remote rule and toggles ntuple-filters,
  which resets some NICs, once per run rather than per case
- huge pages are reserved on both hosts or on neither, and the UNALIGNED
  cases are left out without them
- both endpoints get the DUT's xdp-zc-max-segs for TOO_MANY_FRAGS,
  which is left out without multi-buffer zero-copy

A DUT that does not advertise AF_XDP zero-copy skips the run.

The remote endpoint listens on the remote address, and the DUT endpoint
connects once the remote says on stderr that it listens. The control
connection shares the tested link, where the XDP programs pass it to
the stack. Killing an ssh client does not stop the command it runs, so
timeout(1) bounds the remote endpoint, and the runner waits for it to
exit before the case ends. xskxceiver takes its numeric options as
given, so the runner checks their values before it puts them on the
command lines. The endpoints are waited for with the test_xsk.py
helpers, so the output of one that does not pass is logged, timeouts
included, and the DUT endpoint is killed if the wait for it is
interrupted. They get a traffic and link timeout of 20s, as a NIC can
take seconds to bring its link back after an MTU or XDP change.

The endpoints detach their XDP programs and restore the MTU when they
exit; the runner detaches the programs and restores the MTU only after
one fails or is killed. An endpoint that skips a case tells its peer,
which skips it too, and the case is reported as skipped unless an
endpoint fails.

The runner sizes the rings for HW_SW_MIN_RING_SIZE, a TX ring of 64
descriptors on the sender, and for HW_SW_MAX_RING_SIZE, the largest TX
ring on the sender and RX ring on the receiver, with ethtool -G, and
restores them after the case however the endpoints end. It retries
while the driver returns EBUSY, as ice does until the buffer pool of a
closed XSK socket is released, which happens in a work item. A case
skips when ethtool cannot read the rings that it sizes.

xsk.py runs the xskxceiver that net/lib builds, which drv-net already
installs, and copies it to the remote with cfg.remote.deploy(). Add
xsk.py to TEST_PROGS and to the AF_XDP entry in MAINTAINERS,
CONFIG_XDP_SOCKETS to the drivers/net/hw config, and the xskxceiver
helpers to the drivers/net/hw library exports.

Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 MAINTAINERS                                   |   1 +
 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 tools/testing/selftests/drivers/net/hw/config |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   5 +
 tools/testing/selftests/drivers/net/hw/xsk.py | 368 ++++++++++++++++++
 tools/testing/selftests/net/lib/Makefile      |   2 +-
 .../selftests/net/lib/xsk/xskxceiver.c        |   3 +
 7 files changed, 380 insertions(+), 1 deletion(-)
 create mode 100755 tools/testing/selftests/drivers/net/hw/xsk.py

diff --git a/MAINTAINERS b/MAINTAINERS
index 6985a42bc09c..69c158d42877 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29783,6 +29783,7 @@ F:	include/net/xsk_buff_pool.h
 F:	include/uapi/linux/if_xdp.h
 F:	include/uapi/linux/xdp_diag.h
 F:	net/xdp/
+F:	tools/testing/selftests/drivers/net/hw/xsk.py
 F:	tools/testing/selftests/net/*xsk*
 F:	tools/testing/selftests/net/lib/py/xsk.py
 F:	tools/testing/selftests/net/lib/xsk/
diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index 18ce235a77f8..02e48e74f6c6 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -52,6 +52,7 @@ TEST_PROGS = \
 	uso.py \
 	vlan.py \
 	xdp_metadata.py \
+	xsk.py \
 	xsk_reconfig.py \
 	#
 
diff --git a/tools/testing/selftests/drivers/net/hw/config b/tools/testing/selftests/drivers/net/hw/config
index c6c2b64bb712..f28335170d6b 100644
--- a/tools/testing/selftests/drivers/net/hw/config
+++ b/tools/testing/selftests/drivers/net/hw/config
@@ -26,4 +26,5 @@ CONFIG_UDMABUF=y
 CONFIG_USER_NS=y
 CONFIG_VLAN_8021Q=m
 CONFIG_VXLAN=y
+CONFIG_XDP_SOCKETS=y
 CONFIG_XFRM_USER=y
diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index 4077f5f1a1be..aee6cdb4575b 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -26,6 +26,8 @@ try:
         fd_read_timeout, ip, rand_port, rand_ports, wait_port_listen, \
         wait_file, ctl_file_write, tool
     from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
+    from net.lib.py import xsk_reserve_hugepages, xsk_release_hugepages, \
+        xsk_setup_busy_poll, xsk_wait_endpoint_exit, xsk_wait_listen
     from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
     from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \
         ksft_setup, ksft_variants, KsftNamedVariant
@@ -43,6 +45,9 @@ try:
                "fd_read_timeout", "ip", "rand_port", "rand_ports",
                "wait_port_listen", "wait_file", "ctl_file_write", "tool",
                "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
+               "xsk_reserve_hugepages", "xsk_release_hugepages",
+               "xsk_setup_busy_poll", "xsk_wait_endpoint_exit",
+               "xsk_wait_listen",
                "KsftSkipEx", "KsftFailEx", "KsftXfailEx",
                "ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run",
                "ksft_setup", "ksft_variants", "KsftNamedVariant",
diff --git a/tools/testing/selftests/drivers/net/hw/xsk.py b/tools/testing/selftests/drivers/net/hw/xsk.py
new file mode 100755
index 000000000000..dbcd687fea3d
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/xsk.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+AF_XDP zero-copy tests against an SKB-mode peer.
+
+Each case runs a zero-copy xskxceiver endpoint on NETIF and an SKB-mode one
+on the remote interface. The endpoints send UDP over IPv6 if LOCAL_V6 and
+REMOTE_V6 are set, else over IPv4, each to the MAC address of its next hop
+to the other. The remote endpoint listens on a TCP control port on the
+remote address, and the DUT endpoint connects to it once the remote says on
+stderr that it listens. The control connection shares the tested link, and
+the XDP programs of both endpoints pass it to the stack.
+
+The DUT endpoint binds to the last queue and the remote endpoint to queue
+0. Before the cases run, main() takes the DUT queue out of RSS and adds an
+ntuple filter on each host that steers the test UDP flow to the queue of
+the receiving endpoint, and it undoes that however the run ends.
+
+test_xsk runs each case that "xskxceiver -L" lists, in each DUT direction
+listed with it, with the DUT endpoint driven by interrupts, and
+test_xsk_busy_poll runs them again with the DUT endpoint busy polling. The
+variant name starts with the direction of the DUT endpoint. UNALIGNED cases
+are left out without huge pages on both hosts, and TOO_MANY_FRAGS without
+multi-buffer zero-copy on the DUT.
+
+HW_SW_MIN_RING_SIZE and HW_SW_MAX_RING_SIZE expect the TX ring of the
+sending interface, and the latter also the RX ring of the receiving one, at
+given sizes. These cases set them with ethtool -G before the endpoints
+start, and restore them once the endpoints exit or get killed.
+"""
+
+import re
+import shlex
+import sys
+import time
+from contextlib import ExitStack
+
+from lib.py import (CmdExitFailure, EthtoolFamily, KsftFailEx,
+                    KsftNamedVariant, KsftSkipEx, NetdevFamily, NetDrvEpEnv,
+                    NlError, cmd, defer, ethtool, ip, ksft_eq, ksft_exit,
+                    ksft_run, ksft_variants, rand_port, xsk_release_hugepages,
+                    xsk_reserve_hugepages, xsk_setup_busy_poll,
+                    xsk_wait_endpoint_exit, xsk_wait_listen)
+
+CASE_TIMEOUT = 120
+# xskxceiver waits as long for its peer to connect.
+LISTEN_TIMEOUT = 30
+# Killing ssh leaves the remote endpoint running, so timeout(1) bounds it.
+REMOTE_TIMEOUT = LISTEN_TIMEOUT + CASE_TIMEOUT
+SKIP_CODE = 4
+# Traffic and link timeout of the endpoints, as a NIC can take seconds to
+# bring its link back after an MTU or XDP change.
+XSK_TIMEOUT = 20
+# The TX ring of the sender and the RX ring of the receiver that a case
+# needs at a given size, 64 being the default batch size of xskxceiver.
+RING_SIZES = {
+    "HW_SW_MIN_RING_SIZE": {"tx": 64},
+    "HW_SW_MAX_RING_SIZE": {"tx": "max", "rx": "max"},
+}
+RING_BUSY_TIMEOUT = 5
+
+
+def _get_cases(cfg):
+    """
+    each test case from xskxceiver -L comes out in a form below:
+    0: SEND_RECEIVE rx
+    0: SEND_RECEIVE tx
+    """
+    ret = []
+    out = cmd([str(cfg.bin_local), "-L"]).stdout
+    cases = re.findall(r"^(\d+): (\w+) (rx|tx)$", out, re.MULTILINE)
+    for test_id, name, direction in cases:
+        if "UNALIGNED" in name and not cfg.hugepages:
+            continue
+        if "TOO_MANY_FRAGS" in name and cfg.max_frags <= 1:
+            continue
+        ret.append(KsftNamedVariant(f"{direction}_{name}", direction,
+                                    int(test_id), name))
+    return ret
+
+
+def _restore_rss(cfg, indir):
+    """Restore the RSS indirection table of the DUT. Reset it to the default
+    first, which leaves it to the driver again, and write indir back only if
+    it was not the default."""
+    ethtool(f"-X {cfg.ifname} default")
+    req = {"header": {"dev-index": cfg.ifindex}}
+    if cfg.ethnl.rss_get(req).get("indir") != indir:
+        cfg.ethnl.rss_set({**req, "indir": indir})
+
+
+def _reserve_queue(cfg, undo):
+    """Take the last queue with both an RX and a TX ring out of RSS until
+    undo, return None if the DUT cannot. A TX endpoint fills no RX buffers,
+    so in zero-copy mode its queue drops what it receives, the control
+    connection included."""
+    req = {"header": {"dev-index": cfg.ifindex}}
+    try:
+        ch = cfg.ethnl.channels_get(req)
+        rss = cfg.ethnl.rss_get(req)
+    except NlError:
+        print(f"1..0 # SKIP cannot get the channels or RSS of {cfg.ifname}")
+        return None
+    count = ch.get("combined-count", 0) + min(ch.get("rx-count", 0),
+                                              ch.get("tx-count", 0))
+    if count < 2:
+        print("1..0 # SKIP Test requires NETIF with at least 2 channels")
+        return None
+    queue = count - 1
+    if cmd(f"ethtool -X {cfg.ifname} equal {queue}", fail=False).ret:
+        print(f"1..0 # SKIP {cfg.ifname} cannot take queue {queue} out of RSS")
+        return None
+    undo.callback(_restore_rss, cfg, rss.get("indir"))
+    return queue
+
+
+def _steer_udp(cfg, undo, ifname, queue, host=None):
+    """Steer the test flow to queue until undo, return False if ifname
+    cannot."""
+    ntuple = ethtool(f"-k {ifname}", json=True, host=host)[0]["ntuple-filters"]
+    if not ntuple["active"]:
+        if ntuple["fixed"]:
+            print(f"1..0 # SKIP {ifname} does not support ntuple-filters")
+            return False
+
+        ethtool(f"-K {ifname} ntuple-filters on", host=host)
+        undo.callback(ethtool, f"-K {ifname} ntuple-filters off", host=host)
+
+    res = cmd(f"ethtool -N {ifname} flow-type udp{cfg.addr_ipver} "
+              f"dst-port {cfg.udp_port} action {queue}", host=host, fail=False)
+    if res.ret:
+        print(f"1..0 # SKIP {ifname} rejects the ntuple rule")
+        return False
+    rule = re.search(r"ID (\d+)", res.stdout).group(1)
+    undo.callback(ethtool, f"-N {ifname} delete {rule}", host=host)
+    return True
+
+
+def _next_hop_mac(cfg, addr, peer_mac, ifname, host=None):
+    """Return the MAC address that host sends packets for addr to: peer_mac
+    if addr is on link, else the MAC address of the gateway."""
+    route = ip(f"-{cfg.addr_ipver} route get {addr}", json=True,
+               host=host)[0]
+    gw = route.get("gateway")
+    if not gw:
+        return peer_mac
+
+    # Resolve the gateway, from ifname as an IPv6 one is likely link-local.
+    # The ping only triggers ARP or ND, so the gateway may ignore it.
+    cmd(f"ping -c1 -W1 -I{ifname} {gw}", host=host, fail=False)
+    # Unlike "neigh get", "neigh show" has JSON output in older iproute2.
+    neigh = ip(f"neigh show {gw} dev {ifname}", json=True, host=host)
+    if not neigh or "lladdr" not in neigh[0]:
+        raise KsftFailEx(f"cannot resolve the MAC address of gateway {gw}")
+    return neigh[0]["lladdr"]
+
+
+def _set_ring(ifname, ring, size, host=None):
+    """Set a ring of ifname with ethtool -G. A driver can refuse that while
+    an XSK socket that just closed still holds a queue, as ice does until a
+    work item releases the buffer pool of the socket, so retry for a while."""
+    deadline = time.monotonic() + RING_BUSY_TIMEOUT
+    while True:
+        res = cmd(f"ethtool -G {ifname} {ring} {size}", host=host, fail=False)
+        if res.ret == 0:
+            return
+        if "Device or resource busy" not in res.stderr or \
+           time.monotonic() > deadline:
+            raise CmdExitFailure("Command failed", res)
+        time.sleep(0.1)
+
+
+def _size_rings(cfg, name, direction):
+    """Set the rings that case name needs, and restore them after it."""
+    for ring, size in RING_SIZES[name].items():
+        # The DUT endpoint sends for tx, and receives for rx.
+        if ring == direction:
+            host, ifname = None, cfg.ifname
+        else:
+            host, ifname = cfg.remote, cfg.remote_ifname
+        try:
+            cur = ethtool(f"-g {ifname}", json=True, host=host)[0]
+        except CmdExitFailure:
+            cur = {}
+        if ring not in cur:
+            raise KsftSkipEx(f"cannot read the {ring} ring size of {ifname}")
+        if size == "max":
+            size = cur[f"{ring}-max"]
+        if cur[ring] == size:
+            continue
+        _set_ring(ifname, ring, size, host)
+        defer(_set_ring, ifname, ring, cur[ring], host)
+
+
+def _setup(cfg, name, direction, busy_poll):
+    for link in (cfg.dev, cfg.remote_dev):
+        if "xdp" in link:
+            raise KsftSkipEx(f"{link['ifname']} already has an XDP program")
+    if busy_poll:
+        xsk_setup_busy_poll(cfg.ifname)
+    if "RING_SIZE" in name:
+        _size_rings(cfg, name, direction)
+
+    # Defers run in reverse, so the XDP programs, which can cap the MTU, go
+    # first.
+    fallbacks = [
+        defer(ip, f"link set dev {cfg.ifname} mtu {cfg.dev['mtu']}"),
+        defer(ip, f"link set dev {cfg.remote_ifname} "
+                  f"mtu {cfg.remote_dev['mtu']}", host=cfg.remote),
+        defer(ip, f"link set dev {cfg.ifname} xdpdrv off"),
+        defer(ip, f"link set dev {cfg.remote_ifname} xdpgeneric off",
+              host=cfg.remote),
+    ]
+    return fallbacks
+
+
+def _check_opt(opt, val, low, high):
+    """xskxceiver takes its numeric options as given, so check that val, the
+    value of opt, is in low-high before it is passed on."""
+    if not low <= val <= high:
+        raise ValueError(f"xskxceiver {opt} {val} is not in {low}-{high}")
+
+
+def _fixup_args(cfg, test_id, ctrl_port):
+    cfg.args[cfg.args.index("-t") + 1] = str(test_id)
+    cfg.args[cfg.args.index("-P") + 1] = str(ctrl_port)
+
+
+def _run(cfg, local_args, remote_args, ctrl_port):
+    """Start the remote endpoint, and the DUT one once the remote listens.
+    Return their exit codes, None for a DUT endpoint that did not start."""
+    remote = cmd(shlex.join(remote_args), host=cfg.remote, background=True,
+                 fail=False)
+    head = ""
+    dut_ret = None
+    try:
+        head, listening = xsk_wait_listen(remote, ctrl_port, LISTEN_TIMEOUT)
+        if listening:
+            local = cmd(local_args, background=True, fail=False)
+            dut_ret = xsk_wait_endpoint_exit(local, "dut", CASE_TIMEOUT)
+    finally:
+        # Even if the DUT endpoint did not run, so that the remote one is
+        # gone before the next case starts.
+        remote_ret = xsk_wait_endpoint_exit(remote, "remote", REMOTE_TIMEOUT,
+                                            head)
+    return dut_ret, remote_ret
+
+
+def _test(cfg, direction, test_id, name, busy_poll):
+    ctrl_port = rand_port()
+    _check_opt("-P", ctrl_port, 1, 0xffff)
+
+    fallbacks = _setup(cfg, name, direction, busy_poll)
+    _fixup_args(cfg, test_id, ctrl_port)
+
+    remote = [*cfg.remote_args,
+              "-e", "tx" if direction == "rx" else "rx",
+              *cfg.args]
+    local = [*cfg.local_args,
+             "-e", direction,
+             *(["-b"] if busy_poll else []),
+             *cfg.args]
+
+    dut_ret, remote_ret = _run(cfg, local, remote, ctrl_port)
+    if dut_ret is None:
+        raise KsftFailEx(f"remote endpoint did not listen on port {ctrl_port}")
+    rets = {dut_ret, remote_ret}
+    if rets <= {0, SKIP_CODE}:
+        # Both endpoints detached their XDP programs and restored the MTU.
+        for fallback in fallbacks:
+            fallback.cancel()
+    # An endpoint that skips the case tells its peer, which skips it too.
+    if SKIP_CODE in rets and rets <= {0, SKIP_CODE}:
+        raise KsftSkipEx("xskxceiver skipped the case")
+    ksft_eq(dut_ret, 0, "DUT endpoint exit code")
+    ksft_eq(remote_ret, 0, "remote endpoint exit code")
+
+
+def test_xsk(cfg, direction, test_id, name):
+    _test(cfg, direction, test_id, name, busy_poll=False)
+
+
+def test_xsk_busy_poll(cfg, direction, test_id, name):
+    _test(cfg, direction, test_id, name, busy_poll=True)
+
+
+def main():
+    # defer() works only within a case, so undo restores what main() sets up
+    # for all of them, however the run ends.
+    with NetDrvEpEnv(__file__, nsim_test=False) as cfg, ExitStack() as undo:
+        cfg.bin_local = cfg.net_lib_dir / "xskxceiver"
+        if not cfg.bin_local.exists():
+            print(f"1..0 # SKIP {cfg.bin_local} was not built")
+            sys.exit(SKIP_CODE)
+
+        cfg.ethnl = EthtoolFamily()
+        cfg.netnl = NetdevFamily()
+
+        dev = cfg.netnl.dev_get({"ifindex": cfg.ifindex})
+        if "xsk-zerocopy" not in dev["xdp-features"]:
+            print("1..0 # SKIP NIC does not advertise AF_XDP zero-copy")
+            sys.exit(SKIP_CODE)
+
+        cfg.max_frags = dev.get("xdp-zc-max-segs", 1)
+        cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
+
+        local_dmac = _next_hop_mac(cfg, cfg.remote_addr,
+                                   cfg.remote_dev["address"], cfg.ifname)
+        remote_dmac = _next_hop_mac(cfg, cfg.addr, cfg.dev["address"],
+                                    cfg.remote_ifname, cfg.remote)
+
+        cfg.queue = _reserve_queue(cfg, undo)
+        if cfg.queue is None:
+            sys.exit(SKIP_CODE)
+
+        cfg.udp_port = rand_port()
+        steered = (_steer_udp(cfg, undo, cfg.ifname, cfg.queue) and
+                   _steer_udp(cfg, undo, cfg.remote_ifname, 0, cfg.remote))
+        if not steered:
+            sys.exit(SKIP_CODE)
+
+        saved_hugepages = []
+        undo.callback(xsk_release_hugepages, saved_hugepages)
+        cfg.hugepages = (xsk_reserve_hugepages(saved_hugepages) and
+                         xsk_reserve_hugepages(saved_hugepages, cfg.remote))
+        if not cfg.hugepages:
+            xsk_release_hugepages(saved_hugepages)
+            saved_hugepages.clear()
+
+        cases = _get_cases(cfg)
+
+        _check_opt("--max-frags", cfg.max_frags, 1, 0xffff)
+        _check_opt("--udp-port", cfg.udp_port, 1, 0xffff)
+        _check_opt("--timeout", XSK_TIMEOUT, 1, 0xffff)
+        _check_opt("--queue", cfg.queue, 0, 0xffffffff)
+
+        # _fixup_args() sets the options that differ between the cases.
+        cfg.args = ["-p", cfg.remote_addr,
+                    "--max-frags", str(cfg.max_frags),
+                    "--udp-port", str(cfg.udp_port),
+                    "--timeout", str(XSK_TIMEOUT),
+                    "-t", "0",
+                    "-P", "0"]
+        cfg.remote_args = ["timeout", str(REMOTE_TIMEOUT),
+                           str(cfg.bin_remote),
+                           "-i", cfg.remote_ifname,
+                           "-m", "skb",
+                           "--listen",
+                           "--queue", "0",
+                           "--src-ip", cfg.remote_addr,
+                           "--dst-ip", cfg.addr,
+                           "--dst-mac", remote_dmac]
+        cfg.local_args = [str(cfg.bin_local),
+                          "-i", cfg.ifname,
+                          "-m", "zc",
+                          "--queue", str(cfg.queue),
+                          "--src-ip", cfg.addr,
+                          "--dst-ip", cfg.remote_addr,
+                          "--dst-mac", local_dmac]
+
+        ksft_run([ksft_variants(cases)(test_xsk),
+                  ksft_variants(cases)(test_xsk_busy_poll)], args=(cfg,))
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile
index 49dac776202b..c2524cacffd3 100644
--- a/tools/testing/selftests/net/lib/Makefile
+++ b/tools/testing/selftests/net/lib/Makefile
@@ -34,7 +34,7 @@ include ../../lib.mk
 
 include ../bpf.mk
 
-# AF_XDP test engine used by net/test_xsk.py.
+# AF_XDP test engine shared by net/test_xsk.py and drivers/net/hw/xsk.py.
 XSK_DIR := xsk
 XSK_BPF_OBJ := $(OUTPUT)/xsk_xdp_progs.bpf.o
 XSK_SKEL := $(OUTPUT)/xsk_xdp_progs.skel.h
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index c52e3e376bc5..a4a66c9ebde2 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -55,6 +55,9 @@
  *    l. If multi-buffer is supported, try various nasty combinations of descriptors to
  *       check if they pass the validation or not
  *
+ * drivers/net/hw/xsk.py runs the hardware cases on a physical device in
+ * zero-copy mode, with an SKB-mode peer on a remote host.
+ *
  * Flow:
  * -----
  * - test_xsk.py starts two processes: Tx and Rx
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v4 net-next 15/15] selftests: xsk: document generic and hardware endpoint runs
  2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
                   ` (13 preceding siblings ...)
  2026-10-16 17:43 ` [PATCH v4 net-next 14/15] selftests: drv-net: test AF_XDP zero-copy with an SKB peer Maciej Fijalkowski
@ 2026-10-16 17:43 ` Maciej Fijalkowski
  14 siblings, 0 replies; 20+ messages in thread
From: Maciej Fijalkowski @ 2026-10-16 17:43 UTC (permalink / raw)
  To: netdev
  Cc: bpf, magnus.karlsson, stfomichev, kuba, pabeni, tushar.vyavahare,
	kerneljasonxing, bjorn, Maciej Fijalkowski

Document the frames that the endpoints exchange, the per-case veth
runner and the two-host hardware setup with a zero-copy DUT and an
SKB-mode remote: how each runner drives the endpoints, what the hardware
runner sets up and restores, what both hosts need and how to run the
cases. Point the xskxceiver header comment to the new README.

Signed-off-by: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
---
 .../testing/selftests/net/lib/xsk/README.rst  | 194 ++++++++++++++++++
 .../selftests/net/lib/xsk/xskxceiver.c        |   2 +
 2 files changed, 196 insertions(+)
 create mode 100644 tools/testing/selftests/net/lib/xsk/README.rst

diff --git a/tools/testing/selftests/net/lib/xsk/README.rst b/tools/testing/selftests/net/lib/xsk/README.rst
new file mode 100644
index 000000000000..00d626125a99
--- /dev/null
+++ b/tools/testing/selftests/net/lib/xsk/README.rst
@@ -0,0 +1,194 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==========
+xskxceiver
+==========
+
+The AF_XDP engine in ``net/lib/xsk`` is built as ``net/lib/xskxceiver``.
+The generic ``net/test_xsk.py`` test runs SKB and DRV modes over veth.
+
+Each ``xskxceiver`` endpoint generates and validates raw UDP frames over
+IPv4 or IPv6. It sends them from ``--src-ip`` to ``--dst-ip``, the address
+of its peer, via ``--dst-mac``, the MAC address of its next hop to the
+peer, and to UDP port ``--udp-port``. Over IPv4, the UDP checksum is
+zero. The UDP source port tells the sockets of ``XDP_SHARED_UMEM`` apart.
+The endpoints do not use AF_INET sockets for the test traffic. The XDP
+programs redirect only UDP packets to ``--udp-port`` and pass all other
+traffic to the stack.
+
+Generic veth test
+=================
+
+Two ``xskxceiver`` processes own the TX and RX veth interfaces. The TX
+process listens on a TCP control port (``--listen``) and the RX process
+connects. The Python runner starts a fresh process pair for each mode and
+case. A small control channel reports readiness, packet progress, skips,
+and aborts. For example::
+
+  make -C tools/testing/selftests/net/lib
+  cd tools/testing/selftests/net
+  sudo ./test_xsk.py
+
+Each case creates its own veth pair and picks a random control port and
+UDP port. The endpoints send UDP over IPv6 between two documentation
+addresses, which the veths do not need to have, and to the MAC address of
+the peer veth.
+``test_xsk`` runs each case that ``xskxceiver -l`` lists in SKB and DRV
+mode, and ``test_xsk_busy_poll`` runs them again with busy polling.
+``XDP_ADJUST_TAIL_SHRINK_MULTI_BUFF`` is left out, as it leaks page_pool
+pages on veth, and so is ``XDP_ADJUST_TAIL_GROW``, as veth leaves no room
+to grow most frames. The runner reserves 32 huge pages of 2 MiB for the
+UNALIGNED cases and restores the count however the run ends, and leaves
+those cases out when it cannot reserve them.
+``-t`` selects cases, e.g. ``-t 'test_xsk.drv_*'``, and ``-l`` lists them.
+
+Hardware zero-copy test
+=======================
+
+``drivers/net/hw/xsk.py`` runs each case that ``xskxceiver -L`` lists, in
+each DUT direction listed with it, and owns the TAP results.
+``test_xsk.h`` groups the cases by the DUT directions in which they run.
+
+It starts one ``xskxceiver`` process on each host for each case. The DUT
+runs in ZC mode; the remote runs in SKB mode and does not need zero-copy.
+RX and TX cases are named separately in TAP, e.g.
+``test_xsk.rx_SEND_RECEIVE`` and ``test_xsk.tx_SEND_RECEIVE``. The
+hardware runner skips a DUT that does not advertise AF_XDP zero-copy. The
+DUT endpoint binds with ``XDP_ZEROCOPY``, which fails instead of falling
+back to copy mode, so an advertised device that cannot bind or run a case
+fails it.
+
+``xsk.py`` starts the remote XSK endpoint first. It listens on a TCP
+control port on the remote address, and the DUT endpoint connects to it
+over the tested link. The listener prints a line on stderr once it
+listens, so ``xsk.py`` starts the DUT endpoint without polling the remote
+for the port. The listener waits 30 seconds for its peer, and ``xsk.py``
+as long for that line. The small per-case channel reports readiness,
+packet progress, skips, and aborts. It has no capabilities or verdict
+handshake. ``xsk.py`` chooses the case, sets the timeouts, and reports
+the result from both process exit codes. Each endpoint sees only its own
+device and mode. One that skips a case tells its peer on the control
+channel, which skips the case as well, and ``xsk.py`` reports a case as
+skipped when one endpoint skips it and the other skips or passes it.
+Killing an SSH client does not stop the command it runs, so the remote
+endpoint runs under ``timeout(1)``, and ``xsk.py`` waits for it to exit
+before the case ends.
+
+The endpoints send UDP over IPv6 when ``LOCAL_V6`` and ``REMOTE_V6`` are
+set and over IPv4 otherwise, between the addresses of the two hosts. Each
+sends to the MAC address of its next hop to the other host, which is the
+peer interface on a shared link and the gateway otherwise. ``xsk.py``
+picks the UDP port at random for the run, so ntuple rules can steer the
+test flow. It takes the last DUT queue that has both an RX and a TX ring
+out of RSS and binds the DUT endpoint to it. For DUT RX cases it steers
+the test flow to that queue, and for DUT TX cases to queue 0 on the
+remote receiver. A TX-only zero-copy socket fills no RX buffers, so its
+queue drops what it receives, and TX cases keep that queue out of RSS as
+well. Accelerated RFS, which ntuple filters turn on in some drivers,
+could still steer the control connection to that queue, so RFS has to
+stay off on the DUT, as it is by default. As the XDP programs pass the
+control connection and all other traffic to the stack, the tested link
+does not have to be otherwise idle.
+
+``xsk.py`` sets up the queue, the ntuple rules and huge pages for the
+UNALIGNED cases once, before the cases run, and undoes them however the
+run ends. The run is skipped when either host has no ntuple support or
+rejects the ntuple rule, and the UNALIGNED cases are left out when either
+host cannot reserve the huge pages. Each endpoint detaches its XDP program
+and restores the MTU before it exits. Cases that do not need larger frames
+default to an MTU of 1500 (``ETH_DATA_LEN``), and the SKB-mode remote
+endpoint only raises its MTU when a case needs a larger one, as changing
+the MTU can reset the NIC. Before the endpoints start, ``xsk.py`` sets
+with ``ethtool -G`` the TX ring of the sending interface to 64 descriptors
+for HW_SW_MIN_RING_SIZE, and for HW_SW_MAX_RING_SIZE the TX ring of the
+sending interface and the RX ring of the receiving one to their maximum
+sizes, which can reset the NIC as well. Once both endpoints are ready,
+each waits for its link before sending, as such a reset can take the
+link down on both hosts, and ``xsk.py`` raises the traffic and link
+timeouts of the endpoints to 20 seconds with ``--timeout``. When the run
+ends, the runner resets the RSS indirection table of the DUT with
+``ethtool -X default``, writes the saved table back if it was not the
+default, and restores the ntuple setting and the huge-page count. It
+restores the ring sizes after the case however the endpoints end, and
+after an endpoint fails or is killed, it also detaches the XDP program
+and restores the MTU. A host whose ring sizes ``ethtool -g`` cannot read
+skips the two ring cases.
+The runner does not change channel counts. A DUT with fewer than two
+queues that have both rings skips all cases, as it has no queue to take
+out of RSS. So does a DUT that cannot take one out of RSS.
+
+The remote is either another host (``REMOTE_TYPE=ssh``) or the peer port
+of the same host moved to a network namespace (``REMOTE_TYPE=netns``).
+The framework runs the remote commands for either backend. Both endpoints
+run as root.
+
+Setup
+-----
+
+Build the engine on the DUT::
+
+  make -C tools/testing/selftests/net/lib
+
+A top-level selftests build with ``TARGETS=drivers/net/hw`` includes
+``net/lib`` as well.
+
+Configure the usual driver test variables in
+``tools/testing/selftests/drivers/net/hw/net.config`` or the environment::
+
+  NETIF=eth0
+  LOCAL_V4=192.0.2.1
+  REMOTE_V4=192.0.2.2
+  REMOTE_TYPE=netns
+  REMOTE_ARGS=xsk-peer
+
+Set ``LOCAL_V6`` and ``REMOTE_V6`` as well to test over IPv6, with the
+addresses added ``nodad``. ``REMOTE_ARGS`` names the namespace that holds
+the peer port. With eth0 connected back to back to eth1 of the same host,
+set it up as root::
+
+  ip netns add xsk-peer
+  ip link set eth1 netns xsk-peer
+  ip -n xsk-peer link set lo up
+  ip -n xsk-peer addr add 192.0.2.2/24 dev eth1
+  ip -n xsk-peer link set eth1 up
+  ip addr add 192.0.2.1/24 dev eth0
+  ip link set eth0 up
+
+Run as root on the DUT::
+
+  cd tools/testing/selftests/drivers/net/hw
+  sudo ./xsk.py -t test_xsk.rx_SEND_RECEIVE
+
+``xskxceiver -L`` lists the cases, and ``xsk.py -l`` the names to pass to
+``-t``. Omit ``-t`` to run all cases.
+Set the variables in ``net.config`` when using ``sudo`` so they are
+available to the test process.
+
+``test_xsk`` runs each case with the DUT endpoint driven by interrupts.
+``test_xsk_busy_poll`` runs the same cases again with the DUT endpoint
+busy polling as in the busy-poll pass of ``test_xsk.py``, e.g.
+``-t 'test_xsk_busy_poll.*'``. Each of its cases sets
+``napi_defer_hard_irqs`` and ``gro_flush_timeout`` on the DUT to the
+values ``test_xsk.py`` uses on veth and restores them when it ends. The
+remote endpoint does not busy poll.
+
+With SSH, each case starts its remote endpoint over SSH. SSH connection
+sharing for the remote host (``ControlMaster``, ``ControlPath`` and
+``ControlPersist`` in ssh_config(5)) avoids a new SSH login for each remote
+command. With ``sudo``, that is root's SSH configuration.
+
+The remote host needs AF_XDP in SKB mode, BPF and ``timeout(1)``. In a
+namespace, the remote endpoint runs the DUT's ``xskxceiver`` binary. With
+SSH, ``xsk.py`` copies that binary to the remote, so both hosts must have
+compatible architectures and libraries. The DUT needs RSS support, and
+both hosts need ntuple support. The test uses ``NetDrvEpEnv`` and the
+networking selftest Python helpers for setup and rollback.
+
+The hardware list covers baseline traffic, 2K frames, single packets,
+poll, headroom, invalid TX descriptors, socket statistics, XDP program
+cleanup, XDP drop, metadata, tail adjustment, multi-buffer, 9K,
+unaligned traffic, HW ring sizes, socket teardown, and shared UMEM.
+For ``TOO_MANY_FRAGS``, the runner reads the DUT's ``xdp-zc-max-segs``
+value and passes it to both endpoint processes so the SKB peer validates
+the same packet stream without a capabilities exchange. A DUT without
+multi-buffer zero-copy does not list the case.
diff --git a/tools/testing/selftests/net/lib/xsk/xskxceiver.c b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
index a4a66c9ebde2..e8cd3529d169 100644
--- a/tools/testing/selftests/net/lib/xsk/xskxceiver.c
+++ b/tools/testing/selftests/net/lib/xsk/xskxceiver.c
@@ -9,6 +9,8 @@
  * See test_xsk.py for detailed information on test topology
  * and prerequisite network setup.
  *
+ * See README.rst for the generic and hardware test setup.
+ *
  * Each instance of this test program runs one endpoint, Tx or Rx, with a
  * single socket and a unique UMEM. Two instances validate in-order packet
  * delivery and packet content by sending packets to each other.
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode
  2026-10-16 17:43 ` [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode Maciej Fijalkowski
@ 2026-10-17 17:44   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-17 17:44 UTC (permalink / raw)
  To: Maciej Fijalkowski; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] bpf: Multi-line comments do not start with /* on its own line
--

commit f56c850eb3f84acec00c6d23c81d2796961df979
Author: Maciej Fijalkowski <maciej.fijalkowski@intel.com>

selftests: xsk: drop the single-interface loopback mode

This commit drops the single-interface loopback mode from xsk selftests,
removes the shared-UMEM TX setup and resizing code, and sets up hardware-based
traffic testing using ntuple and RSS configuration instead.

> diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
> index 05457f77ca52c..b9126fe66e1a7 100644
> --- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
> +++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
[ ... ]
> @@ -2481,14 +2379,19 @@ int testapp_xdp_metadata_mb(struct test_spec *test)
>  	return testapp_xdp_metadata_copy(test);
>  }
>  
> +/* Expects the HW TX ring of the sender to hold DEFAULT_BATCH_SIZE
> + * descriptors.
> + */

[Severity: Low]
This isn't a bug, but does the comment for testapp_hw_sw_min_ring_size
in tools/testing/selftests/bpf/prog_tests/test_xsk.c violate the BPF
subsystem multi-line comment style? The BPF guidelines specify that
multi-line comments should start with an empty /* line instead of putting
text on the opening line.

>  int testapp_hw_sw_min_ring_size(struct test_spec *test)
>  {
>  	int ret;
[ ... ]
> @@ -2501,16 +2404,21 @@ int testapp_hw_sw_min_ring_size(struct test_spec *test)
>  	return testapp_validate_traffic(test);
>  }
>  
> +/* Expects the HW TX ring of the sender and the HW RX ring of the receiver
> + * at their maximum sizes.
> + */

[Severity: Low]
This isn't a bug, but does the comment for testapp_hw_sw_max_ring_size
in tools/testing/selftests/bpf/prog_tests/test_xsk.c violate the BPF
subsystem multi-line comment style? Similar to the previous finding, BPF
multi-line comments should start with the opening /* on its own line.

>  int testapp_hw_sw_max_ring_size(struct test_spec *test)
>  {

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016174338.787135-1-maciej.fijalkowski@intel.com?part=3

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE
  2026-10-16 17:43 ` [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE Maciej Fijalkowski
@ 2026-10-17 17:44   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-17 17:44 UTC (permalink / raw)
  To: Maciej Fijalkowski; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf: integer underflow calculating xsk batch_size
- [Low] bpf: multi-line comment does not follow kernel style
--

commit 743c541687fdf2d7592285088f7c482d5114a852
Author: Maciej Fijalkowski <maciej.fijalkowski@intel.com>

selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE

This commit sizes the TX UMEM like the RX one to avoid failures in
dual-interface mode and caps the batch size below the XSK ring size to avoid
infinite loops.

> diff --git a/tools/testing/selftests/bpf/prog_tests/test_xsk.c b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
> index 2a13628740924..05457f77ca52c 100644
> --- a/tools/testing/selftests/bpf/prog_tests/test_xsk.c
> +++ b/tools/testing/selftests/bpf/prog_tests/test_xsk.c
[ ... ]
> @@ -2521,10 +2525,12 @@ int testapp_hw_sw_max_ring_size(struct test_spec *test)
>  		return ret;
>  
>  	/* Set batch_size to 8152 for testing, as the ice HW ignores the 3 lowest bits when
> -	 * updating the Rx HW tail register.
> +	 * updating the Rx HW tail register. __send_pkts() waits until a whole batch fits
> +	 * in the XSK TX ring, so keep it below that ring's size on NICs with larger rings.
>  	 */

[Severity: Low]
This isn't a bug, but does this multi-line comment follow the BPF subsystem
style guidelines?

The guidelines request the opening /* to be on its own line.

> -	test->ifobj_tx->xsk->batch_size = test->ifobj_tx->ring.tx_max_pending - 8;
> -	test->ifobj_rx->xsk->batch_size = test->ifobj_tx->ring.tx_max_pending - 8;
> +	batch_size = min(test->ifobj_tx->ring.tx_max_pending, max_descs) - 8;

[Severity: Medium]
Can this subtraction underflow if tx_max_pending is less than 8?

If an interface supports ethtool get_ringparam but reports a tx_max_pending
smaller than 8 (such as netdevsim altered via debugfs or drivers that leave
it 0), the unsigned subtraction would underflow and produce a massive
batch_size.

Could this cause __send_pkts() to infinite loop or hang the test while trying
to reserve descriptors that exceed the ring capacity?

> +	test->ifobj_tx->xsk->batch_size = batch_size;
> +	test->ifobj_rx->xsk->batch_size = batch_size;
>  	if (pkt_stream_replace(test, max_descs, MIN_PKT_SIZE)) {
>  		clean_sockets(test, test->ifobj_tx);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016174338.787135-1-maciej.fijalkowski@intel.com?part=2

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation
  2026-10-16 17:43 ` [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation Maciej Fijalkowski
@ 2026-10-17 17:44   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-17 17:44 UTC (permalink / raw)
  To: Maciej Fijalkowski; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: missing prerequisites (root, veth) cause hard failure instead of skip
--

commit 621c952e0f68b97c93b16b3262842ed60506e7fc
Author: Maciej Fijalkowski <maciej.fijalkowski@intel.com>

selftests: xsk: run one test case per xskxceiver invocation

Replace test_xsk.sh and xsk_prereqs.sh with test_xsk.py to run xskxceiver once
per mode and case. The new Python script relies on the networking selftests
Python library to manage test cases and resources.

> diff --git a/tools/testing/selftests/net/test_xsk.py b/tools/testing/selftests/net/test_xsk.py
> new file mode 100755
> index 0000000000000..80ac67cdebb66
> --- /dev/null
> +++ b/tools/testing/selftests/net/test_xsk.py
[ ... ]
> +def _veth_pair(busy_poll):
> +    pfx = f"xsk{random.randrange(1 << 20):05x}"
> +    veths = (pfx + "a", pfx + "b")
> +    ip(f"link add {veths[0]} numtxqueues 4 numrxqueues 4 type veth "
> +       f"peer name {veths[1]} numtxqueues 4 numrxqueues 4")

[Severity: Medium]
When the test suite is run as a non-root user or on a system without veth
module support, will this cause a hard failure rather than a skip?

The previous xsk_prereqs.sh script explicitly checked for root privileges and
veth support, returning ksft_skip gracefully if they were missing. Here, the
ip() command will raise a CmdExitFailure exception if the privileges or driver
are missing, which the test runner catches and reports as a failure.

Could this turn an environment difference into a false regression signal?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016174338.787135-1-maciej.fijalkowski@intel.com?part=9

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs
  2026-10-16 17:43 ` [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs Maciej Fijalkowski
@ 2026-10-17 17:44   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-17 17:44 UTC (permalink / raw)
  To: Maciej Fijalkowski; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] bpf: multi-line comment style violation
--

commit c93ec22f2ead9dd358b23391b62cb4c9037e31b7
Author: Maciej Fijalkowski <maciej.fijalkowski@intel.com>

    selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs
    
    xskxceiver sends frames of the loopback ethertype between made-up MAC
    addresses, and its XDP programs redirect every frame to the XSKMAP.
    That suits a veth pair that carries nothing else, but not a physical
    link, where the peer NIC filters out the made-up addresses and the
    programs take neighbour discovery, LLDP or SSH away from the stack.
    
    Send the test packets as UDP between the MAC addresses of the two
    interfaces on any link. Each endpoint takes the IPv4 or IPv6 addresses
    of the flow (--src-ip, --dst-ip), the MAC address of its peer
    (--dst-mac) and the UDP port (--udp-port), and reads its own MAC
    address from the interface. Over IPv4 the UDP checksum is left at zero,
    as IPv4 allows; over IPv6 it is computed from the known payload.
    PKT_HDR_SIZE and MIN_PKT_SIZE now stand for variables that
    xsk_set_flow() sets, so the smallest packet grows with the headers to
    64 bytes over IPv4 and 88 over IPv6, and a verbatim stream adds up the
    descriptors of a packet for its IP and UDP lengths.

> diff --git a/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c b/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
> index 023d8befd4cab..2b55dbaf13b24 100644
> --- a/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
> +++ b/tools/testing/selftests/net/lib/xsk/xsk_xdp_progs.bpf.c
> @@ -15,12 +23,64 @@ struct {
>  	__uint(value_size, sizeof(int));
>  } xsk SEC(".maps");
>  
> -static unsigned int idx;
>  int adjust_value = 0;
>  int count = 0;
> +/* UDP destination port of the test flow, other traffic goes to the stack */
> +__u16 test_port;
> +
> +/* Return the UDP header of a test packet, and in hdr_len the size of the
> + * headers in front of its payload, or NULL for any other packet.
> + */

[Severity: Low]
This is a minor coding style observation. The BPF subsystem prefers the
opening of multi-line comments to be on its own line. Should this be
formatted as follows?

/*
 * Return the UDP header of a test packet, and in hdr_len the size of the
 * headers in front of its payload, or NULL for any other packet.
 */

> +static __always_inline struct udphdr *test_udp_hdr(struct xdp_md *xdp,
> +						   __u32 *hdr_len)
> +{
> +	void *data_end = (void *)(long)xdp->data_end;
> +	void *data = (void *)(long)xdp->data;
> +	struct ethhdr *eth = data;
> +	struct ipv6hdr *ip6;
> +	struct udphdr *udp;
> +	struct iphdr *ip;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261016174338.787135-1-maciej.fijalkowski@intel.com?part=11

^ permalink raw reply	[flat|nested] 20+ messages in thread

end of thread, other threads:[~2026-10-17 17:44 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 17:43 [PATCH v4 net-next 00/15] selftests: net: migrate AF_XDP test suite over to net Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 01/15] selftests: xsk: factor endpoint work out of pthread wrappers Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 02/15] selftests: xsk: size the TX UMEM in HW_SW_MAX_RING_SIZE Maciej Fijalkowski
2026-10-17 17:44   ` sashiko-bot
2026-10-16 17:43 ` [PATCH v4 net-next 03/15] selftests: xsk: drop the single-interface loopback mode Maciej Fijalkowski
2026-10-17 17:44   ` sashiko-bot
2026-10-16 17:43 ` [PATCH v4 net-next 04/15] selftests/bpf: drop the test_progs AF_XDP wrapper Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 05/15] selftests: net: add a generic rule for BPF skeletons Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 06/15] selftests: xsk: move the AF_XDP test suite to selftests/net Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 07/15] selftests: xsk: collect interface capabilities in struct xsk_caps Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 08/15] selftests: xsk: split xskxceiver main() into setup, run and cleanup Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 09/15] selftests: xsk: run one test case per xskxceiver invocation Maciej Fijalkowski
2026-10-17 17:44   ` sashiko-bot
2026-10-16 17:43 ` [PATCH v4 net-next 10/15] selftests: xsk: run the RX and TX endpoints in separate processes Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 11/15] selftests: xsk: send UDP over IPv4 or IPv6 between the interface MACs Maciej Fijalkowski
2026-10-17 17:44   ` sashiko-bot
2026-10-16 17:43 ` [PATCH v4 net-next 12/15] selftests: xsk: prepare xskxceiver for endpoints on two hosts Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 13/15] selftests: xsk: list the hardware test cases Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 14/15] selftests: drv-net: test AF_XDP zero-copy with an SKB peer Maciej Fijalkowski
2026-10-16 17:43 ` [PATCH v4 net-next 15/15] selftests: xsk: document generic and hardware endpoint runs Maciej Fijalkowski

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