* Re: [RFC PATCH net-next] selftests: net: exercise virtio checksum ingress outcomes
2026-10-17 6:00 [RFC PATCH net-next] selftests: net: exercise virtio checksum ingress outcomes Megane Hebo
@ 2026-10-17 6:02 ` Megane Hebo
2026-10-17 15:55 ` Willem de Bruijn
0 siblings, 1 reply; 4+ messages in thread
From: Megane Hebo @ 2026-10-17 6:02 UTC (permalink / raw)
To: netdev
Cc: linux-kselftest, davem, edumazet, kuba, pabeni, horms, shuah, mst,
virtualization, willemdebruijn.kernel, habte.yibelo, jasowangio,
eperezma, xuanzhuo
[-- Attachment #1: Type: text/plain, Size: 7186 bytes --]
Correction: Gmail Web rewrote tabs and wrapped some long lines in the
inline diff I just sent, so that copy is not suitable for applying.
The attached format-patch file is the authoritative byte-exact patch.
Sorry for the transport noise.
SHA256: cd11c3970f0a23ac039cdfcdc6338c20c79dbff3db8fdc93ea53b4b8996437b8
On Sat, 17 Oct 2026 15:00:02 +0900, Megane Hebo <hebomegane@gmail.com> wrote:
> The virtio checksum hardening series gives virtio_net_hdr_to_skb()
> three outcomes for VIRTIO_NET_HDR_F_NEEDS_CSUM: known-good partial
> checksum metadata is retained, uncommon non-GSO metadata is resolved
> in software at ingress, and GSO metadata that cannot be resolved safely
> is rejected.
>
> Add focused TUN tests for these three outcomes. Keep the packet shape
> otherwise identical (IPv4/UDP, no encapsulation), so each test changes
> only the metadata needed to select a different branch.
>
> For the software-resolution case, verify the rx_csum_sw ethtool counter
> added by the hardening series. A recognized partial checksum must not
> increment it. An uncommon non-GSO checksum must increment it once. A
> GSO/protocol mismatch must be rejected without incrementing it.
>
> These are contract tests for ingress normalization: packet semantics
> must be made consistent before later protocol processing observes the
> packet.
>
> This is intended as a follow-up to [PATCH net-next v2 0/7]
> "more virtio-net csum hardening".
>
> Signed-off-by: Kiyotaka Tonegawa <hebomegane@gmail.com>
> ---
> Testing:
> - applied on current upstream master after Michael's v2 0/7 series
> - built tools/testing/selftests/net/tun with the normal YNL dependencies
> - runtime against a kernel containing v2: not run
>
> tools/testing/selftests/net/tun.c | 160 ++++++++++++++++++++++++++++++
> 1 file changed, 160 insertions(+)
>
> diff --git a/tools/testing/selftests/net/tun.c
> b/tools/testing/selftests/net/tun.c
> index afaa81c..8775315 100644
> --- a/tools/testing/selftests/net/tun.c
> +++ b/tools/testing/selftests/net/tun.c
> @@ -8,7 +8,9 @@
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> +#include <linux/ethtool.h>
> #include <linux/if_tun.h>
> +#include <linux/sockios.h>
> #include <netinet/tcp.h>
> #include <sys/ioctl.h>
> #include <sys/socket.h>
> @@ -543,6 +545,76 @@ TEST_F(tun, reattach_close_delete)
> EXPECT_EQ(tun_delete(self->ifname), 0);
> }
>
> +static int tun_get_stat(const char *ifname, const char *name, uint64_t *value)
> +{
> + struct ethtool_drvinfo drvinfo = { .cmd = ETHTOOL_GDRVINFO };
> + struct ethtool_gstrings *strings = NULL;
> + struct ethtool_stats *stats = NULL;
> + struct ifreq ifr = {};
> + unsigned int i;
> + int fd, ret;
> +
> + fd = socket(AF_INET, SOCK_DGRAM, 0);
> + if (fd < 0)
> + return -errno;
> +
> + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
> + ifr.ifr_data = (void *)&drvinfo;
> + if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
> + ret = -errno;
> + goto out;
> + }
> +
> + if (!drvinfo.n_stats) {
> + ret = -ENOENT;
> + goto out;
> + }
> +
> + strings = calloc(1, sizeof(*strings) +
> + drvinfo.n_stats * ETH_GSTRING_LEN);
> + stats = calloc(1, sizeof(*stats) +
> + drvinfo.n_stats * sizeof(stats->data[0]));
> + if (!strings || !stats) {
> + ret = -ENOMEM;
> + goto out;
> + }
> +
> + strings->cmd = ETHTOOL_GSTRINGS;
> + strings->string_set = ETH_SS_STATS;
> + strings->len = drvinfo.n_stats;
> + ifr.ifr_data = (void *)strings;
> + if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
> + ret = -errno;
> + goto out;
> + }
> +
> + stats->cmd = ETHTOOL_GSTATS;
> + stats->n_stats = drvinfo.n_stats;
> + ifr.ifr_data = (void *)stats;
> + if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
> + ret = -errno;
> + goto out;
> + }
> +
> + ret = -ENOENT;
> + for (i = 0; i < drvinfo.n_stats; i++) {
> + const char *stat_name =
> + (const char *)&strings->data[i * ETH_GSTRING_LEN];
> +
> + if (!strncmp(stat_name, name, ETH_GSTRING_LEN)) {
> + *value = stats->data[i];
> + ret = 0;
> + break;
> + }
> + }
> +
> +out:
> + free(stats);
> + free(strings);
> + close(fd);
> + return ret;
> +}
> +
> FIXTURE(tun_vnet_gso)
> {
> char ifname[IFNAMSIZ];
> @@ -611,6 +683,35 @@ static int build_vlan_tcpv4_gso_packet(uint8_t
> *buf, int payload_len)
> return cur - buf;
> }
>
> +static int build_ipv4_udp_partial_packet(uint8_t *buf, int payload_len,
> + uint8_t gso_type,
> + uint16_t csum_offset)
> +{
> + uint8_t *cur = buf + sizeof(struct virtio_net_hdr);
> + struct virtio_net_hdr vh = { 0 };
> +
> + cur += build_eth(cur, ETH_P_IP, param_hwaddr_outer_src,
> + param_hwaddr_outer_dst);
> + cur += build_ipv4_header(cur, IPPROTO_UDP,
> + sizeof(struct udphdr) + payload_len,
> + ¶m_ipaddr4_outer_src,
> + ¶m_ipaddr4_outer_dst);
> + cur += build_udp_packet(cur, UDP_SRC_PORT, UDP_DST_PORT,
> + payload_len, AF_INET, true);
> +
> + vh.flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
> + vh.hdr_len = ETH_HLEN + sizeof(struct iphdr) + sizeof(struct udphdr);
> + vh.csum_start = ETH_HLEN + sizeof(struct iphdr);
> + vh.csum_offset = csum_offset;
> + vh.gso_type = gso_type;
> + if (gso_type != VIRTIO_NET_HDR_GSO_NONE)
> + vh.gso_size = 64;
> +
> + memcpy(buf, &vh, sizeof(vh));
> +
> + return cur - buf;
> +}
> +
> TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum)
> {
> struct virtio_net_hdr vh;
> @@ -678,6 +779,65 @@ TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum_64k)
> ASSERT_EQ(ret, len);
> }
>
> +TEST_F(tun_vnet_gso, csum_partial_known_good)
> +{
> + uint8_t pkt[512] = { 0 };
> + uint64_t before, after;
> + int len, ret;
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
> +
> + len = build_ipv4_udp_partial_packet(pkt, 128,
> + VIRTIO_NET_HDR_GSO_NONE,
> + __builtin_offsetof(struct
> udphdr, check));
> + ret = write(self->fd, pkt, len);
> + ASSERT_EQ(ret, len);
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
> + EXPECT_EQ(after, before);
> +}
> +
> +TEST_F(tun_vnet_gso, csum_partial_uncommon_resolved)
> +{
> + uint8_t pkt[512] = { 0 };
> + uint64_t before, after;
> + int len, ret;
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
> +
> + len = build_ipv4_udp_partial_packet(pkt, 128,
> + VIRTIO_NET_HDR_GSO_NONE,
> + __builtin_offsetof(struct
> udphdr, source));
> + ret = write(self->fd, pkt, len);
> + ASSERT_EQ(ret, len);
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
> + EXPECT_EQ(after, before + 1);
> +}
> +
> +TEST_F(tun_vnet_gso, csum_partial_gso_protocol_mismatch_rejected)
> +{
> + uint8_t pkt[512] = { 0 };
> + uint64_t before, after;
> + int len, ret;
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
> +
> + /* The checksum field is a valid UDP checksum field, but the
> + * virtio header claims TCPv4 GSO. GSO cannot be resolved as an
> + * arbitrary checksum write, so the ingress contract rejects it.
> + */
> + len = build_ipv4_udp_partial_packet(pkt, 256,
> + VIRTIO_NET_HDR_GSO_TCPV4,
> + __builtin_offsetof(struct
> udphdr, check));
> + ret = write(self->fd, pkt, len);
> + ASSERT_EQ(ret, -1);
> + ASSERT_EQ(errno, EINVAL);
> +
> + ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
> + EXPECT_EQ(after, before);
> +}
> +
> FIXTURE(tun_vnet_udptnl)
> {
> char ifname[IFNAMSIZ];
> --
> 2.53.0
[-- Attachment #2: 0001-selftests-net-exercise-virtio-checksum-ingress-outcomes.patch --]
[-- Type: text/x-patch, Size: 6732 bytes --]
From ba75eb8388f3f058196b636b58a2b5783a683156 Mon Sep 17 00:00:00 2001
From: Kiyotaka Tonegawa <hebomegane@gmail.com>
Date: Sat, 17 Oct 2026 13:21:40 +0900
Subject: [RFC PATCH net-next] selftests: net: exercise virtio checksum ingress
outcomes
The virtio checksum hardening series gives virtio_net_hdr_to_skb()
three outcomes for VIRTIO_NET_HDR_F_NEEDS_CSUM: known-good partial
checksum metadata is retained, uncommon non-GSO metadata is resolved
in software at ingress, and GSO metadata that cannot be resolved safely
is rejected.
Add focused TUN tests for these three outcomes. Keep the packet shape
otherwise identical (IPv4/UDP, no encapsulation), so each test changes
only the metadata needed to select a different branch.
For the software-resolution case, verify the rx_csum_sw ethtool counter
added by the hardening series. A recognized partial checksum must not
increment it. An uncommon non-GSO checksum must increment it once. A
GSO/protocol mismatch must be rejected without incrementing it.
These are contract tests for ingress normalization: packet semantics
must be made consistent before later protocol processing observes the
packet.
This is intended as a follow-up to [PATCH net-next v2 0/7]
"more virtio-net csum hardening".
Signed-off-by: Kiyotaka Tonegawa <hebomegane@gmail.com>
---
Testing:
- applied on current upstream master after Michael's v2 0/7 series
- built tools/testing/selftests/net/tun with the normal YNL dependencies
- runtime against a kernel containing v2: not run
tools/testing/selftests/net/tun.c | 160 ++++++++++++++++++++++++++++++
1 file changed, 160 insertions(+)
diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index afaa81c..8775315 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -8,7 +8,9 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
+#include <linux/ethtool.h>
#include <linux/if_tun.h>
+#include <linux/sockios.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
@@ -543,6 +545,76 @@ TEST_F(tun, reattach_close_delete)
EXPECT_EQ(tun_delete(self->ifname), 0);
}
+static int tun_get_stat(const char *ifname, const char *name, uint64_t *value)
+{
+ struct ethtool_drvinfo drvinfo = { .cmd = ETHTOOL_GDRVINFO };
+ struct ethtool_gstrings *strings = NULL;
+ struct ethtool_stats *stats = NULL;
+ struct ifreq ifr = {};
+ unsigned int i;
+ int fd, ret;
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0)
+ return -errno;
+
+ strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
+ ifr.ifr_data = (void *)&drvinfo;
+ if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
+ ret = -errno;
+ goto out;
+ }
+
+ if (!drvinfo.n_stats) {
+ ret = -ENOENT;
+ goto out;
+ }
+
+ strings = calloc(1, sizeof(*strings) +
+ drvinfo.n_stats * ETH_GSTRING_LEN);
+ stats = calloc(1, sizeof(*stats) +
+ drvinfo.n_stats * sizeof(stats->data[0]));
+ if (!strings || !stats) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ strings->cmd = ETHTOOL_GSTRINGS;
+ strings->string_set = ETH_SS_STATS;
+ strings->len = drvinfo.n_stats;
+ ifr.ifr_data = (void *)strings;
+ if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
+ ret = -errno;
+ goto out;
+ }
+
+ stats->cmd = ETHTOOL_GSTATS;
+ stats->n_stats = drvinfo.n_stats;
+ ifr.ifr_data = (void *)stats;
+ if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) {
+ ret = -errno;
+ goto out;
+ }
+
+ ret = -ENOENT;
+ for (i = 0; i < drvinfo.n_stats; i++) {
+ const char *stat_name =
+ (const char *)&strings->data[i * ETH_GSTRING_LEN];
+
+ if (!strncmp(stat_name, name, ETH_GSTRING_LEN)) {
+ *value = stats->data[i];
+ ret = 0;
+ break;
+ }
+ }
+
+out:
+ free(stats);
+ free(strings);
+ close(fd);
+ return ret;
+}
+
FIXTURE(tun_vnet_gso)
{
char ifname[IFNAMSIZ];
@@ -611,6 +683,35 @@ static int build_vlan_tcpv4_gso_packet(uint8_t *buf, int payload_len)
return cur - buf;
}
+static int build_ipv4_udp_partial_packet(uint8_t *buf, int payload_len,
+ uint8_t gso_type,
+ uint16_t csum_offset)
+{
+ uint8_t *cur = buf + sizeof(struct virtio_net_hdr);
+ struct virtio_net_hdr vh = { 0 };
+
+ cur += build_eth(cur, ETH_P_IP, param_hwaddr_outer_src,
+ param_hwaddr_outer_dst);
+ cur += build_ipv4_header(cur, IPPROTO_UDP,
+ sizeof(struct udphdr) + payload_len,
+ ¶m_ipaddr4_outer_src,
+ ¶m_ipaddr4_outer_dst);
+ cur += build_udp_packet(cur, UDP_SRC_PORT, UDP_DST_PORT,
+ payload_len, AF_INET, true);
+
+ vh.flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
+ vh.hdr_len = ETH_HLEN + sizeof(struct iphdr) + sizeof(struct udphdr);
+ vh.csum_start = ETH_HLEN + sizeof(struct iphdr);
+ vh.csum_offset = csum_offset;
+ vh.gso_type = gso_type;
+ if (gso_type != VIRTIO_NET_HDR_GSO_NONE)
+ vh.gso_size = 64;
+
+ memcpy(buf, &vh, sizeof(vh));
+
+ return cur - buf;
+}
+
TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum)
{
struct virtio_net_hdr vh;
@@ -678,6 +779,65 @@ TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum_64k)
ASSERT_EQ(ret, len);
}
+TEST_F(tun_vnet_gso, csum_partial_known_good)
+{
+ uint8_t pkt[512] = { 0 };
+ uint64_t before, after;
+ int len, ret;
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
+
+ len = build_ipv4_udp_partial_packet(pkt, 128,
+ VIRTIO_NET_HDR_GSO_NONE,
+ __builtin_offsetof(struct udphdr, check));
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, len);
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
+ EXPECT_EQ(after, before);
+}
+
+TEST_F(tun_vnet_gso, csum_partial_uncommon_resolved)
+{
+ uint8_t pkt[512] = { 0 };
+ uint64_t before, after;
+ int len, ret;
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
+
+ len = build_ipv4_udp_partial_packet(pkt, 128,
+ VIRTIO_NET_HDR_GSO_NONE,
+ __builtin_offsetof(struct udphdr, source));
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, len);
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
+ EXPECT_EQ(after, before + 1);
+}
+
+TEST_F(tun_vnet_gso, csum_partial_gso_protocol_mismatch_rejected)
+{
+ uint8_t pkt[512] = { 0 };
+ uint64_t before, after;
+ int len, ret;
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &before), 0);
+
+ /* The checksum field is a valid UDP checksum field, but the
+ * virtio header claims TCPv4 GSO. GSO cannot be resolved as an
+ * arbitrary checksum write, so the ingress contract rejects it.
+ */
+ len = build_ipv4_udp_partial_packet(pkt, 256,
+ VIRTIO_NET_HDR_GSO_TCPV4,
+ __builtin_offsetof(struct udphdr, check));
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ ASSERT_EQ(tun_get_stat(self->ifname, "rx_csum_sw", &after), 0);
+ EXPECT_EQ(after, before);
+}
+
FIXTURE(tun_vnet_udptnl)
{
char ifname[IFNAMSIZ];
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread