Index | Thread | Search

From:
Jan Klemkow <jan@openbsd.org>
Subject:
vmd: checksum offloading
To:
mlarkin@openbsd.org, dv@openbsd.org, claudio@openbsd.org
Cc:
dlg@openbsd.org, bluhm@openbsd.org, tech@openbsd.org
Date:
Sun, 20 Sep 2026 19:04:37 +0200

Download raw body.

Thread
Hi,

This diff brings checksum offload to vmd(8) and is a requirement for TSO/LRO.
Its a rebase on the current tree, after mlarkin and dv have done a lot of
changes in vmd(8) during the w2k26.

It contains most comments from claudio besides the ioctl(2) of TUNSCAP.
Because the feature negotiation will happen later on runtime and with
VIRTIO_NET_F_CTRL_GUEST_OFFLOADS it will also change dynamically by the guest.
GUEST_OFFLOADS is a hard requirement for LRO later.  So, I keep it,
as dv wishes to keep the vionet process in pledge stdio-only mode.

After mlarkin sends me his AI-mangled version of my TSO diff, I got inspired
and have done some simplification of the header parsing part.

bluhm: can you run this in netlink?

ok?

Thanks,
jan

Index: sys/kern/kern_pledge.c
===================================================================
RCS file: /cvs/src/sys/kern/kern_pledge.c,v
diff -u -p -r1.368 kern_pledge.c
--- sys/kern/kern_pledge.c	19 Sep 2026 17:21:52 -0000	1.368
+++ sys/kern/kern_pledge.c	20 Sep 2026 04:06:14 -0000
@@ -48,6 +48,7 @@
 #include <net/route.h>
 #include <net/if.h>
 #include <net/if_var.h>
+#include <net/if_tun.h>
 #include <netinet/in.h>
 #include <netinet6/in6_var.h>
 #include <netinet6/nd6.h>
@@ -1380,6 +1381,12 @@ pledge_ioctl(struct proc *p, long com, s
 			case VMM_IOC_CREATE:
 				return (0);
 			}
+		}
+		if ((fp->f_type == DTYPE_VNODE) &&
+		    (vp->v_type == VCHR) &&
+		    (cdevsw[major(vp->v_rdev)].d_open == tapopen)) {
+			if (com == TUNSCAP)
+				return 0;
 		}
 	}
 #endif
Index: usr.sbin/vmd/vionet.c
===================================================================
RCS file: /cvs/src/usr.sbin/vmd/vionet.c,v
diff -u -p -r1.36 vionet.c
--- usr.sbin/vmd/vionet.c	19 Sep 2026 17:21:52 -0000	1.36
+++ usr.sbin/vmd/vionet.c	20 Sep 2026 16:18:28 -0000
@@ -17,12 +17,18 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include <sys/types.h>
+#include <sys/ioctl.h>
 
 #include <dev/pci/virtio_pcireg.h>
 #include <dev/pv/virtioreg.h>
 
 #include <net/if.h>
+#include <net/if_tun.h>
 #include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
 #include <netinet/if_ether.h>
 
 #include <errno.h>
@@ -50,6 +56,7 @@
 
 #define VIRTIO_NET_CONFIG_MAC		 0 /*  8 bit x 6 byte */
 
+#define VIRTIO_NET_F_GUEST_CSUM	(1 << 1)
 #define VIRTIO_NET_F_MAC	(1 << 5)
 #define RXQ	0
 #define TXQ	1
@@ -65,7 +72,7 @@ static void *rx_run_loop(void *);
 static void *tx_run_loop(void *);
 static int vionet_rx(struct virtio_dev *, int);
 static ssize_t vionet_rx_copy(struct vionet_dev *, int, const struct iovec *,
-    int, size_t);
+    int, size_t, struct tun_hdr *th);
 static ssize_t vionet_rx_zerocopy(struct vionet_dev *, int,
     const struct iovec *, int);
 static void vionet_rx_event(int, short, void *);
@@ -84,6 +91,10 @@ static void read_pipe_rx(int, short, voi
 static void read_pipe_tx(int, short, void *);
 static void vionet_assert_irq(struct virtio_dev *, uint16_t);
 static void vionet_deassert_pic_irq(struct virtio_dev *);
+static void vhdr2thdr(struct virtio_net_hdr *, struct tun_hdr *,
+    const struct iovec *, int);
+static void thdr2vhdr(struct tun_hdr *, struct virtio_net_hdr *,
+    const struct iovec *, int);
 
 /* Device Globals */
 struct event ev_tap;
@@ -167,7 +178,7 @@ vionet_main(int fd, int vm_fd)
 	 * We no longer need VM fd access.
 	 */
 	close_fd(vm_fd);
-	if (pledge("stdio", NULL) == -1)
+	if (pledge("stdio vmm", NULL) == -1)
 		fatal("pledge2");
 
 	/* Initialize our packet injection pipe. */
@@ -299,6 +310,31 @@ fail:
 }
 
 /*
+ * Update and sync offload features with tap(4).
+ */
+static void
+vionet_update_offload(struct virtio_dev *dev)
+{
+	struct viodev_msg	msg;
+	int			ret;
+
+	memset(&msg, 0, sizeof(msg));
+	msg.irq = dev->irq;
+	msg.type = VIODEV_MSG_TUNSCAP;
+
+	if (dev->driver_feature & VIRTIO_NET_F_GUEST_CSUM) {
+		msg.data |= IFCAP_CSUM_TCPv4 | IFCAP_CSUM_UDPv4;
+		msg.data |= IFCAP_CSUM_TCPv6 | IFCAP_CSUM_UDPv6;
+	}
+
+	ret = imsg_compose_event2(&dev->async_iev, IMSG_DEVOP_MSG, 0, 0, -1,
+	    &msg, sizeof(msg), ev_base_main);
+	if (ret == -1)
+		log_warnx("%s: failed to assert irq %d", __func__, dev->irq);
+}
+
+
+/*
  * vionet_rx
  *
  * Pull packet from the provided fd and fill the receive-side virtqueue. We
@@ -320,6 +356,7 @@ vionet_rx(struct virtio_dev *dev, int fd
 	struct virtio_net_hdr *hdr = NULL;
 	struct virtio_vq_info *vq_info;
 	struct iovec *iov;
+	struct tun_hdr th;
 	int notify = 0;
 	ssize_t sz;
 	uint8_t status = 0;
@@ -351,8 +388,8 @@ vionet_rx(struct virtio_dev *dev, int fd
 			goto reset;
 		}
 
-		iov = &iov_rx[0];
-		iov_cnt = 1;
+		iov = &iov_rx[1];
+		iov_cnt = 2;
 
 		/*
 		 * First descriptor should be at least as large as the
@@ -373,7 +410,6 @@ vionet_rx(struct virtio_dev *dev, int fd
 		if (iov->iov_base == NULL)
 			goto reset;
 		hdr = iov->iov_base;
-		memset(hdr, 0, sizeof(struct virtio_net_hdr));
 
 		/* Tweak the iovec to account for the virtio_net_hdr. */
 		iov->iov_len -= sizeof(struct virtio_net_hdr);
@@ -418,15 +454,15 @@ vionet_rx(struct virtio_dev *dev, int fd
 			goto reset;
 		}
 
-		hdr->num_buffers = iov_cnt;
-
 		/*
 		 * If we're enforcing hardware address or handling an injected
 		 * packet, we need to use a copy-based approach.
 		 */
+		iov_rx[0].iov_base = &th;
+		iov_rx[0].iov_len = sizeof(th);
 		if (vionet->lockedmac || fd != vionet->data_fd)
-			sz = vionet_rx_copy(vionet, fd, iov_rx, iov_cnt,
-			    chain_len);
+			sz = vionet_rx_copy(vionet, fd, iov_rx + 1, iov_cnt - 1,
+			    chain_len, &th);
 		else
 			sz = vionet_rx_zerocopy(vionet, fd, iov_rx, iov_cnt);
 		if (sz == -1)
@@ -434,6 +470,9 @@ vionet_rx(struct virtio_dev *dev, int fd
 		if (sz == 0)	/* No packets, so bail out for now. */
 			break;
 
+		thdr2vhdr(&th, hdr, iov_rx + 1, iov_cnt - 1);
+		hdr->num_buffers = iov_cnt - 1;
+
 		/*
 		 * Account for the prefixed header since it wasn't included
 		 * in the copy or zerocopy operations.
@@ -473,9 +512,9 @@ reset:
  */
 ssize_t
 vionet_rx_copy(struct vionet_dev *dev, int fd, const struct iovec *iov,
-    int iov_cnt, size_t chain_len)
+    int iov_cnt, size_t chain_len, struct tun_hdr *th)
 {
-	static uint8_t		 buf[VIONET_HARD_MTU];
+	static uint8_t		 buf[sizeof(struct tun_hdr) + VIONET_HARD_MTU];
 	struct packet		*pkt = NULL;
 	struct ether_header	*eh = NULL;
 	uint8_t			*payload = buf;
@@ -484,7 +523,8 @@ vionet_rx_copy(struct vionet_dev *dev, i
 
 	/* If reading from the tap(4), try to right-size the read. */
 	if (fd == dev->data_fd)
-		nbytes = MIN(chain_len, VIONET_HARD_MTU);
+		nbytes = sizeof(struct tun_hdr) +
+		    MIN(chain_len, VIONET_HARD_MTU);
 	else if (fd == pipe_inject[READ])
 		nbytes = sizeof(struct packet);
 	else {
@@ -504,10 +544,20 @@ vionet_rx_copy(struct vionet_dev *dev, i
 			return (-1);
 		}
 		return (0);
-	} else if (fd == dev->data_fd && sz < VIONET_MIN_TXLEN) {
+	} else if (fd == dev->data_fd) {
+		if ((size_t)sz < sizeof(struct tun_hdr)) {
+			log_warnx("%s: short tun_hdr", __func__);
+			return (0);
+		}
+		memcpy(th, payload, sizeof *th);
+		payload += sizeof(struct tun_hdr);
+		sz -= sizeof(struct tun_hdr);
+
 		/* If reading the tap(4), we should get valid ethernet. */
-		log_warnx("%s: invalid packet size", __func__);
-		return (0);
+		if (sz < VIONET_MIN_TXLEN) {
+			log_warnx("%s: invalid packet size", __func__);
+			return (0);
+		}
 	} else if (fd == pipe_inject[READ] && sz != sizeof(struct packet)) {
 		log_warnx("%s: invalid injected packet object (sz=%ld)",
 		    __func__, sz);
@@ -526,6 +576,7 @@ vionet_rx_copy(struct vionet_dev *dev, i
 			log_warnx("%s: invalid injected packet size", __func__);
 			goto drop;
 		}
+		memset(th, 0, sizeof *th);
 		payload = pkt->buf;
 		sz = (ssize_t)pkt->len;
 	}
@@ -585,6 +636,12 @@ vionet_rx_zerocopy(struct vionet_dev *de
 	sz = readv(fd, iov, iov_cnt);
 	if (sz == -1 && errno == EAGAIN)
 		return (0);
+
+	if ((size_t)sz < sizeof(struct tun_hdr))
+		return (0);
+
+	sz -= sizeof(struct tun_hdr);
+
 	return (sz);
 }
 
@@ -673,6 +730,8 @@ vionet_tx(struct virtio_dev *dev)
 	struct ether_header *eh;
 	struct iovec *iov;
 	struct packet pkt;
+	struct virtio_net_hdr *vhp;
+	struct tun_hdr th;
 	uint8_t status = 0;
 
 	status = dev->status & VIRTIO_CONFIG_DEVICE_STATUS_DRIVER_OK;
@@ -701,8 +760,9 @@ vionet_tx(struct virtio_dev *dev)
 			goto reset;
 		}
 
-		iov = &iov_tx[0];
-		iov_cnt = 0;
+		/* the 0th slot will by used by the tun_hdr */
+		iov = &iov_tx[1];
+		iov_cnt = 1;
 		chain_len = 0;
 
 		/*
@@ -713,13 +773,17 @@ vionet_tx(struct virtio_dev *dev)
 			log_warnx("%s: invalid descriptor length", __func__);
 			goto reset;
 		}
-		iov->iov_len = desc->len;
 
-		if (iov->iov_len > sizeof(struct virtio_net_hdr)) {
-			/* Chop off the virtio header, leaving packet data. */
-			iov->iov_len -= sizeof(struct virtio_net_hdr);
-			iov->iov_base = hvaddr_mem(desc->addr +
-			    sizeof(struct virtio_net_hdr), iov->iov_len);
+		/* Chop the virtio net header off */
+		vhp = hvaddr_mem(desc->addr, sizeof(*vhp));
+		if (vhp == NULL)
+			goto reset;
+
+		iov->iov_len = desc->len - sizeof(*vhp);
+		if (iov->iov_len > 0) {
+			iov->iov_base = hvaddr_mem(desc->addr + sizeof(*vhp),
+			    iov->iov_len);
+
 			if (iov->iov_base == NULL)
 				goto reset;
 
@@ -767,7 +831,7 @@ vionet_tx(struct virtio_dev *dev)
 		 * descriptor with packet data contains a large enough buffer
 		 * for this inspection.
 		 */
-		iov = &iov_tx[0];
+		iov = &iov_tx[1];
 		if (vionet->lockedmac) {
 			if (iov->iov_len < ETHER_HDR_LEN) {
 				log_warnx("%s: insufficient header data",
@@ -793,6 +857,15 @@ vionet_tx(struct virtio_dev *dev)
 			}
 		}
 
+		/*
+		 * if we look at more of vhp we might need to copy
+		 * it so it's aligned properly
+		 */
+		vhdr2thdr(vhp, &th, iov_tx + 1, iov_cnt - 1);
+
+		iov_tx[0].iov_base = &th;
+		iov_tx[0].iov_len = sizeof(th);
+
 		/* Write our packet to the tap(4). */
 		sz = writev(vionet->data_fd, iov_tx, iov_cnt);
 		if (sz == -1 && errno != ENOBUFS) {
@@ -1122,6 +1195,7 @@ vionet_cfg_write(struct virtio_dev *dev,
 		dev->driver_feature &= dev->device_feature;
 		DPRINTF("%s: driver features 0x%llx", __func__,
 		    dev->driver_feature);
+		vionet_update_offload(dev);
 		break;
 	case VIO1_PCI_CONFIG_MSIX_VECTOR:
 		if (sz != 2)
@@ -1598,4 +1672,122 @@ vionet_deassert_pic_irq(struct virtio_de
 	    &msg, sizeof(msg), ev_base_main);
 	if (ret == -1)
 		log_warnx("%s: failed to assert irq %d", __func__, dev->irq);
+}
+
+static int
+iov_copydata(const struct iovec *iov, int iovcnt, size_t off, size_t len,
+    void *buf)
+{
+	uint8_t *cp = buf;
+	size_t count;
+
+	/* inspired by m_getptr() */
+	while (off > 0) {
+		if (iovcnt == 0)
+			return (-1);
+
+		if (off < iov->iov_len)
+			break;
+
+		off -= iov->iov_len;
+		iov++;
+		iovcnt--;
+	}
+
+	/* inspired by m_copydata() */
+	while (len > 0) {
+		if (iovcnt == 0)
+			return (-1);
+		count = MIN(iov->iov_len - off, len);
+		memmove(cp, (const uint8_t *)iov->iov_base + off, count);
+		len -= count;
+		cp += count;
+		off = 0;
+		iov++;
+		iovcnt--;
+	}
+
+	return (0);
+}
+
+static size_t
+hdr_off(const struct iovec *iov, int iovcnt)
+{
+	size_t		off = 0;
+	uint16_t	etype;
+
+	if (iov_copydata(iov, iovcnt, offsetof(struct ether_header, ether_type),
+	    sizeof(etype), &etype) == -1)
+		return 0;
+
+	off = sizeof(struct ether_header);
+
+	if (etype == htons(ETHERTYPE_VLAN)) {
+		if (iov_copydata(iov, iovcnt,
+		    offsetof(struct ether_vlan_header, evl_proto),
+		    sizeof(etype), &etype) == -1)
+			return 0;
+
+		off = sizeof(struct ether_vlan_header);
+	}
+
+	if (etype == htons(ETHERTYPE_IP)) {
+		size_t	offs;
+		uint8_t	hl;
+
+		/* Get ipproto field from IP header. */
+		offs = off + offsetof(struct ip, ip_p);
+
+		/* Get IP header length field from IP header. */
+		offs = off;
+		if (iov_copydata(iov, iovcnt, offs, sizeof(hl), &hl) == -1)
+			return 0;
+
+		off += (hl & 0x0f) << 2;
+	} else if (etype == htons(ETHERTYPE_IPV6)) {
+		off += sizeof(struct ip6_hdr);
+	}
+
+	return off;
+}
+
+static void
+vhdr2thdr(struct virtio_net_hdr *vh, struct tun_hdr *th,
+    const struct iovec *iov, int iovcnt)
+{
+	memset(th, 0, sizeof(*th));
+
+	if (vh->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) {
+		switch (vh->csum_offset) {
+		case offsetof(struct tcphdr, th_sum):
+			th->th_flags |= TUN_H_TCP_CSUM;
+			break;
+
+		case offsetof(struct udphdr, uh_sum):
+			th->th_flags |= TUN_H_UDP_CSUM;
+			break;
+		}
+	}
+}
+
+static void
+thdr2vhdr(struct tun_hdr *th, struct virtio_net_hdr *vh,
+    const struct iovec *iov, int iovcnt)
+{
+	memset(vh, 0, sizeof(*vh));
+
+	if (th->th_flags & (TUN_H_TCP_CSUM | TUN_H_UDP_CSUM)) {
+		vh->flags |= VIRTIO_NET_HDR_F_NEEDS_CSUM;
+		vh->csum_start = hdr_off(iov, iovcnt);
+
+		switch (th->th_flags & (TUN_H_TCP_CSUM | TUN_H_UDP_CSUM)) {
+		case TUN_H_TCP_CSUM:
+			vh->csum_offset = offsetof(struct tcphdr, th_sum);
+			break;
+
+		case TUN_H_UDP_CSUM:
+			vh->csum_offset = offsetof(struct udphdr, uh_sum);
+			break;
+		}
+	}
 }
Index: usr.sbin/vmd/virtio.c
===================================================================
RCS file: /cvs/src/usr.sbin/vmd/virtio.c,v
diff -u -p -r1.153 virtio.c
--- usr.sbin/vmd/virtio.c	19 Sep 2026 17:21:52 -0000	1.153
+++ usr.sbin/vmd/virtio.c	20 Sep 2026 04:06:15 -0000
@@ -19,6 +19,7 @@
 #include <sys/param.h>	/* PAGE_SIZE */
 #include <sys/socket.h>
 #include <sys/wait.h>
+#include <sys/ioctl.h>
 
 #include <dev/pci/pcireg.h>
 #include <dev/pci/pcidevs.h>
@@ -28,6 +29,7 @@
 #include <dev/vmm/vmm.h>
 
 #include <net/if.h>
+#include <net/if_tun.h>
 #include <netinet/in.h>
 #include <netinet/if_ether.h>
 
@@ -75,6 +77,8 @@ SLIST_HEAD(virtio_dev_head, virtio_dev) 
 
 #define MAXPHYS	(64 * 1024)	/* max raw I/O transfer size */
 
+#define VIRTIO_NET_F_CSUM	(1<<0)
+#define VIRTIO_NET_F_GUEST_CSUM	(1<<1)
 #define VIRTIO_NET_F_MAC	(1<<5)
 
 #define VMMCI_F_TIMESYNC	(1<<0)
@@ -1173,6 +1177,8 @@ virtio_init(struct vmd_vm *vm, int child
 	/* Virtio 1.x Network Devices */
 	if (vmc->vmc_nnics > 0) {
 		for (i = 0; i < vmc->vmc_nnics; i++) {
+			struct tun_capabilities tcap;
+
 			dev = malloc(sizeof(struct virtio_dev));
 			if (dev == NULL) {
 				log_warn("calloc failure allocating vionet");
@@ -1188,7 +1194,8 @@ virtio_init(struct vmd_vm *vm, int child
 			}
 			virtio_dev_init(vm, dev, id, VIONET_QUEUE_SIZE_DEFAULT,
 			    VIRTIO_NET_QUEUES,
-			    (VIRTIO_NET_F_MAC | VIRTIO_F_VERSION_1));
+			    (VIRTIO_NET_F_CSUM | VIRTIO_NET_F_GUEST_CSUM |
+			     VIRTIO_NET_F_MAC | VIRTIO_F_VERSION_1));
 
 			bar_id = pci_add_bar(id, PCI_MAPREG_TYPE_IO, virtio_pci_io,
 			    dev);
@@ -1213,6 +1220,14 @@ virtio_init(struct vmd_vm *vm, int child
 			dev->vm_fd = vm->vm_fd;
 			dev->vionet.data_fd = child_taps[i];
 
+			/*
+			 * IFCAPs are tweaked after feature negotiation with
+			 * the guest later.
+			 */
+			memset(&tcap, 0, sizeof(tcap));
+			if (ioctl(dev->vionet.data_fd, TUNSCAP, &tcap) == -1)
+			    fatal("tap(4) TUNSCAP");
+
 			/* MAC address has been assigned by the parent */
 			memcpy(&dev->vionet.mac, &vmc->vmc_macs[i], 6);
 			dev->vionet.lockedmac =
@@ -1718,7 +1733,8 @@ virtio_dev_launch(struct vmd_vm *vm, str
 		}
 
 		/* Close data fds. Only the child device needs them now. */
-		if (virtio_dev_closefds(dev) == -1) {
+		if (dev->dev_type != VMD_DEVTYPE_NET &&
+		    virtio_dev_closefds(dev) == -1) {
 			log_warnx("%s: failed to close device data fds",
 			    __func__);
 			goto err;
@@ -1963,6 +1979,18 @@ handle_dev_msg(struct viodev_msg *msg, s
 	case VIODEV_MSG_ERROR:
 		log_warnx("%s: device reported error", __func__);
 		break;
+	case VIODEV_MSG_TUNSCAP:
+	{
+		struct tun_capabilities tcap;
+
+		memset(&tcap, 0, sizeof(tcap));
+		tcap.tun_if_capabilities = msg->data;
+
+		if (ioctl(gdev->vionet.data_fd, TUNSCAP, &tcap) == -1)
+			fatal("%s: tap(4) TUNSCAP", __func__);
+
+		break;
+	}
 	case VIODEV_MSG_INVALID:
 	case VIODEV_MSG_IO_READ:
 	case VIODEV_MSG_IO_WRITE:
Index: usr.sbin/vmd/virtio.h
===================================================================
RCS file: /cvs/src/usr.sbin/vmd/virtio.h,v
diff -u -p -r1.66 virtio.h
--- usr.sbin/vmd/virtio.h	19 Sep 2026 17:21:52 -0000	1.66
+++ usr.sbin/vmd/virtio.h	20 Sep 2026 04:06:15 -0000
@@ -134,6 +134,7 @@ struct viodev_msg {
 #define VIODEV_MSG_IO_WRITE	5
 #define VIODEV_MSG_DUMP		6
 #define VIODEV_MSG_SHUTDOWN	7
+#define VIODEV_MSG_TUNSCAP	8
 
 	uint16_t reg;		/* VirtIO register */
 	uint8_t io_sz;		/* IO instruction size */
@@ -320,6 +321,9 @@ struct virtio_net_hdr {
 	uint16_t padding_reserved;
 	*/
 };
+
+#define VIRTIO_NET_HDR_F_NEEDS_CSUM	1 /* flags */
+#define VIRTIO_NET_HDR_F_DATA_VALID	2 /* flags */
 
 enum vmmci_cmd {
 	VMMCI_NONE = 0,