Netdev List
 help / color / mirror / Atom feed
From: Antonio Quartulli <antonio@mandelbit.com>
To: willemdebruijn.kernel@gmail.com, jasowangio@gmail.com
Cc: Antonio Quartulli <antonio@mandelbit.com>,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@kernel.org,
	kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
	mst@redhat.com, eperezma@redhat.com, shuah@kernel.org,
	ast@kernel.org, daniel@iogearbox.net, hawk@kernel.org,
	john.fastabend@gmail.com, sdf@fomichev.me, maheshb@google.com,
	peterpenkov96@gmail.com, netdev@vger.kernel.org,
	virtualization@lists.linux.dev, linux-kselftest@vger.kernel.org,
	bpf@vger.kernel.org
Subject: [PATCH net v6 2/2] selftests: net: add a vhost-net test for the tun NAPI queue bound
Date: Fri, 13 Nov 2026 22:22:34 +0100	[thread overview]
Message-ID: <20261113212237.1742984-3-antonio@mandelbit.com> (raw)
In-Reply-To: <20261113212237.1742984-1-antonio@mandelbit.com>

Feed a tap device with IFF_NAPI from vhost-net with netdev_max_backlog
set to 8, in the three ways vhost-net passes packets to tun: batches of
XDP buffers, single packets with MSG_MORE and zerocopy buffers. Check
that excess packets are dropped and counted, that the queue keeps
draining and that every buffer comes back.

The wrapper loads vhost_net and, if it can reload it, also runs the
zerocopy variant with experimental_zcopytx=1.

Signed-off-by: Antonio Quartulli <antonio@mandelbit.com>
---
 tools/testing/selftests/net/.gitignore   |   1 +
 tools/testing/selftests/net/Makefile     |   2 +
 tools/testing/selftests/net/config       |   1 +
 tools/testing/selftests/net/tun_vhost.c  | 222 +++++++++++++++++++++++
 tools/testing/selftests/net/tun_vhost.sh |  23 +++
 5 files changed, 249 insertions(+)
 create mode 100644 tools/testing/selftests/net/tun_vhost.c
 create mode 100755 tools/testing/selftests/net/tun_vhost.sh

diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index 27761239d069..2f4104b1aa6d 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -56,6 +56,7 @@ timestamping
 tls
 tools
 tun
+tun_vhost
 txring_overwrite
 txtimestamp
 udp_splice_checksum
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index baea3e7f0f8f..b3965a976451 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -123,6 +123,7 @@ TEST_PROGS := \
 	test_vxlan_vnifiltering.sh \
 	tfo_passive.sh \
 	traceroute.sh \
+	tun_vhost.sh \
 	txtimestamp.sh \
 	udpgro.sh \
 	udpgro_bench.sh \
@@ -177,6 +178,7 @@ TEST_GEN_FILES := \
 	tcp_mmap \
 	tfo \
 	timestamping \
+	tun_vhost \
 	txring_overwrite \
 	txtimestamp \
 	udpgso \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index 3e7ce41bd64d..c3bb0d572325 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -136,6 +136,7 @@ CONFIG_TRACEPOINTS=y
 CONFIG_TUN=y
 CONFIG_USER_NS=y
 CONFIG_VETH=y
+CONFIG_VHOST_NET=m
 CONFIG_VLAN_8021Q=y
 CONFIG_VXLAN=m
 CONFIG_XFRM_INTERFACE=m
diff --git a/tools/testing/selftests/net/tun_vhost.c b/tools/testing/selftests/net/tun_vhost.c
new file mode 100644
index 000000000000..c9bf6d07cc26
--- /dev/null
+++ b/tools/testing/selftests/net/tun_vhost.c
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Feed a tap device with IFF_NAPI from vhost-net, in batches of XDP buffers,
+ * as single packets with MSG_MORE, or as zerocopy buffers, with
+ * netdev_max_backlog set to 8: excess packets must be dropped and counted,
+ * the queue must keep draining, and every buffer must come back.
+ *
+ * Author: Antonio Quartulli <antonio@mandelbit.com>
+ */
+
+#define _GNU_SOURCE
+#include <endian.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/vhost.h>
+#include <linux/virtio_config.h>
+#include <linux/virtio_net.h>
+#include <linux/virtio_ring.h>
+#include <sys/eventfd.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+#include "kselftest_harness.h"
+
+#define RING	256
+#define BUFSZ	2048
+#define HDRLEN	sizeof(struct virtio_net_hdr_v1)
+#define PKTS	(64 * RING)
+
+static long read_long(const char *fmt, const char *arg, long def)
+{
+	char path[128];
+	long v = def;
+	FILE *f;
+
+	snprintf(path, sizeof(path), fmt, arg);
+	f = fopen(path, "r");
+	if (f) {
+		if (fscanf(f, "%ld", &v) != 1)
+			v = def;
+		fclose(f);
+	}
+	return v;
+}
+
+FIXTURE(tun_vhost)
+{
+	char ifname[IFNAMSIZ];
+	char bl[16];		/* saved netdev_max_backlog */
+	int bl_fd;
+	struct vring vr;
+	int vhost;
+	int kick;
+	int tap;
+};
+
+FIXTURE_VARIANT(tun_vhost)
+{
+	bool limit_sndbuf;	/* no XDP batches */
+	bool zerocopy;
+	int pktlen;
+};
+
+FIXTURE_VARIANT_ADD(tun_vhost, batch)
+{
+	.pktlen = 64,
+};
+
+FIXTURE_VARIANT_ADD(tun_vhost, copy)
+{
+	.limit_sndbuf = true,
+	.pktlen = 64,
+};
+
+FIXTURE_VARIANT_ADD(tun_vhost, zerocopy)
+{
+	.limit_sndbuf = true,
+	.zerocopy = true,
+	.pktlen = 1000,
+};
+
+FIXTURE_SETUP(tun_vhost)
+{
+	struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR |
+					  IFF_NAPI };
+	unsigned long long features = 1ULL << VIRTIO_F_VERSION_1;
+	struct vhost_vring_state state = { .index = 1, .num = RING };
+	struct vhost_vring_file file = { .index = 1 };
+	struct vhost_vring_addr addr = { .index = 1 };
+	int hdrlen = HDRLEN, sndbuf = 1 << 20, i, fd;
+	struct vhost_memory *mem;
+	unsigned char *bufs;
+	void *ring;
+	bool zc;
+
+	self->bl_fd = -1;
+	self->vhost = open("/dev/vhost-net", O_RDWR);
+	if (self->vhost < 0)
+		SKIP(return, "no /dev/vhost-net");
+	zc = read_long("/sys/module/vhost_net/parameters/%s",
+		       "experimental_zcopytx", 0);
+	if (variant->zerocopy && !zc)
+		SKIP(return, "vhost_net experimental_zcopytx not set");
+	if (!variant->limit_sndbuf && zc)
+		SKIP(return, "vhost_net does not batch with zerocopy");
+
+	self->tap = open("/dev/net/tun", O_RDWR);
+	ASSERT_GE(self->tap, 0);
+	strcpy(ifr.ifr_name, "tvh%d");
+	ASSERT_EQ(ioctl(self->tap, TUNSETIFF, &ifr), 0);
+	memcpy(self->ifname, ifr.ifr_name, IFNAMSIZ);
+	ASSERT_EQ(ioctl(self->tap, TUNSETVNETHDRSZ, &hdrlen), 0);
+	if (variant->limit_sndbuf)
+		ASSERT_EQ(ioctl(self->tap, TUNSETSNDBUF, &sndbuf), 0);
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	ASSERT_EQ(ioctl(fd, SIOCGIFFLAGS, &ifr), 0);
+	ifr.ifr_flags |= IFF_UP;
+	ASSERT_EQ(ioctl(fd, SIOCSIFFLAGS, &ifr), 0);
+	close(fd);
+
+	/* each buffer: vnet header and an IPv4 frame for another host, which
+	 * the stack drops without counting it
+	 */
+	ASSERT_EQ(posix_memalign((void **)&bufs, 4096, RING * BUFSZ), 0);
+	memset(bufs, 0, RING * BUFSZ);
+	ASSERT_EQ(posix_memalign(&ring, 4096, vring_size(RING, 4096)), 0);
+	memset(ring, 0, vring_size(RING, 4096));
+	vring_init(&self->vr, RING, ring, 4096);
+	for (i = 0; i < RING; i++) {
+		unsigned char *eth = bufs + i * BUFSZ + HDRLEN;
+
+		eth[0] = 0x02;
+		eth[5] = 0x02;
+		eth[12] = 0x08;
+		eth[14] = 0x45;
+		self->vr.desc[i].addr = htole64((unsigned long)eth - HDRLEN);
+		self->vr.desc[i].len = htole32(HDRLEN + variant->pktlen);
+		self->vr.avail->ring[i] = htole16(i);
+	}
+
+	/* buffer addresses are passed as is: map them 1:1 */
+	mem = calloc(1, sizeof(*mem) + sizeof(mem->regions[0]));
+	ASSERT_NE(mem, NULL);
+	mem->nregions = 1;
+	mem->regions[0].guest_phys_addr = (unsigned long)bufs;
+	mem->regions[0].userspace_addr = (unsigned long)bufs;
+	mem->regions[0].memory_size = RING * BUFSZ;
+
+	self->kick = eventfd(0, 0);
+	ASSERT_GE(self->kick, 0);
+	addr.desc_user_addr = (unsigned long)self->vr.desc;
+	addr.avail_user_addr = (unsigned long)self->vr.avail;
+	addr.used_user_addr = (unsigned long)self->vr.used;
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_OWNER, NULL), 0);
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_FEATURES, &features), 0);
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_MEM_TABLE, mem), 0);
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_VRING_NUM, &state), 0);
+	state.num = 0;
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_VRING_BASE, &state), 0);
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_VRING_ADDR, &addr), 0);
+	file.fd = self->kick;
+	ASSERT_EQ(ioctl(self->vhost, VHOST_SET_VRING_KICK, &file), 0);
+	file.fd = self->tap;
+	ASSERT_EQ(ioctl(self->vhost, VHOST_NET_SET_BACKEND, &file), 0);
+	free(mem);
+}
+
+FIXTURE_TEARDOWN(tun_vhost)
+{
+	/* restore first: closing vhost blocks if buffers are not returned */
+	if (self->bl_fd >= 0 &&
+	    pwrite(self->bl_fd, self->bl, strlen(self->bl), 0) < 0)
+		TH_LOG("cannot restore netdev_max_backlog to %s", self->bl);
+	close(self->vhost);
+	close(self->tap);
+}
+
+TEST_F(tun_vhost, napi_queue_bounded)
+{
+	const char *stat = "/sys/class/net/%s/statistics/rx_packets";
+	const char *drop = "/sys/class/net/%s/statistics/rx_dropped";
+	long rx = read_long(stat, self->ifname, -1);
+	long dropped = read_long(drop, self->ifname, -1);
+	unsigned short idx = 0;
+	int n, ms;
+
+	/* lowered only here, so that a failed setup leaves it alone */
+	self->bl_fd = open("/proc/sys/net/core/netdev_max_backlog", O_RDWR);
+	ASSERT_GE(self->bl_fd, 0);
+	n = pread(self->bl_fd, self->bl, sizeof(self->bl) - 1, 0);
+	ASSERT_GT(n, 0);
+	self->bl[n] = '\0';
+	ASSERT_EQ(pwrite(self->bl_fd, "8", 1, 0), 1);
+
+	/* post the whole ring, wait for every buffer to come back, repeat */
+	for (n = 0; n < PKTS; n += RING) {
+		idx += RING;
+		__atomic_store_n(&self->vr.avail->idx, htole16(idx),
+				 __ATOMIC_RELEASE);
+		ASSERT_EQ(eventfd_write(self->kick, 1), 0);
+		for (ms = 0; le16toh(__atomic_load_n(&self->vr.used->idx,
+						     __ATOMIC_ACQUIRE)) != idx;
+		     ms++) {
+			ASSERT_LT(ms, 5000) TH_LOG("buffers not returned");
+			usleep(1000);
+		}
+	}
+
+	rx = read_long(stat, self->ifname, -1) - rx;
+	dropped = read_long(drop, self->ifname, -1) - dropped;
+	TH_LOG("sent %d, delivered %ld, dropped %ld", PKTS, rx, dropped);
+	EXPECT_GT(dropped, 0);		/* bounded */
+	EXPECT_GE(rx, PKTS / 16);	/* draining while the sender sends */
+	EXPECT_EQ(rx + dropped, PKTS);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/net/tun_vhost.sh b/tools/testing/selftests/net/tun_vhost.sh
new file mode 100755
index 000000000000..d92707de5e29
--- /dev/null
+++ b/tools/testing/selftests/net/tun_vhost.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Run tun_vhost with vhost_net loaded, and once more with zerocopy enabled
+# if the module can be reloaded.
+
+ksft_skip=4
+param=/sys/module/vhost_net/parameters/experimental_zcopytx
+
+modprobe -q vhost_net
+[ -e /dev/vhost-net ] || exit $ksft_skip
+
+./tun_vhost
+ret=$?
+
+zc=$(cat "$param" 2>/dev/null)
+if [ "$zc" = 0 ] && modprobe -q -r vhost_net 2>/dev/null; then
+	modprobe vhost_net experimental_zcopytx=1 &&
+		{ ./tun_vhost -v zerocopy || ret=1; }
+	modprobe -q -r vhost_net
+	modprobe vhost_net experimental_zcopytx="$zc"
+fi
+exit $ret
-- 
2.56.0


  parent reply	other threads:[~2026-11-13 21:22 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-13 21:22 [PATCH net v6 0/2] tun: bound the NAPI receive queue Antonio Quartulli
2026-11-13 21:22 ` [PATCH net v6 1/2] " Antonio Quartulli
2026-11-13 21:22 ` Antonio Quartulli [this message]
2026-11-13 21:28 ` [PATCH net v6 0/2] " netdev-bot+sinfo

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261113212237.1742984-3-antonio@mandelbit.com \
    --to=antonio@mandelbit.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=eperezma@redhat.com \
    --cc=hawk@kernel.org \
    --cc=horms@kernel.org \
    --cc=jasowangio@gmail.com \
    --cc=john.fastabend@gmail.com \
    --cc=kuba@kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=maheshb@google.com \
    --cc=mst@redhat.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=peterpenkov96@gmail.com \
    --cc=sdf@fomichev.me \
    --cc=shuah@kernel.org \
    --cc=virtualization@lists.linux.dev \
    --cc=willemdebruijn.kernel@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox