From 6c0305c7ae4a821a1250c47094b664194d5640e5 Mon Sep 17 00:00:00 2001 From: JackDoan Date: Mon, 27 Jul 2026 15:38:21 -0500 Subject: [PATCH] overlay/tio: derive WriteGSO geometry from non-empty fragments GSOSize came from len(pays[0]) while the iovec build skips empty fragments, so a leading empty fragment emitted a TSO/USO header with gso_size == 0 -- virtio_net_hdr_to_skb rejects that with EINVAL and the whole superpacket is lost. Compute gso_size from the first non-empty fragment and use the non-empty count to decide superpacket vs plain. Co-Authored-By: Claude Fable 5 --- overlay/tio/tio_gso_linux.go | 18 +++++++++-- overlay/tio/tun_linux_offload_test.go | 43 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/overlay/tio/tio_gso_linux.go b/overlay/tio/tio_gso_linux.go index 64ca75e6..c4762f54 100644 --- a/overlay/tio/tio_gso_linux.go +++ b/overlay/tio/tio_gso_linux.go @@ -334,14 +334,28 @@ func (r *Offload) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte, proto default: csumOff = 16 } + // GSO geometry comes from the non-empty fragments only: the iovec loop + // below skips empties, so gso_size must never be derived from one. A + // leading empty fragment would otherwise stamp a superpacket header + // with gso_size == 0, which the kernel rejects with EINVAL. + segSize, segCount := 0, 0 + for _, p := range pays { + if len(p) == 0 { + continue + } + if segCount == 0 { + segSize = len(p) + } + segCount++ + } vhdr := virtio.Hdr{ Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, HdrLen: uint16(len(hdr) + len(transportHdr)), - GSOSize: uint16(len(pays[0])), + GSOSize: uint16(segSize), CsumStart: uint16(len(hdr)), CsumOffset: csumOff, } - if len(pays) > 1 { + if segCount > 1 { ipVer := hdr[0] >> 4 switch { case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6): diff --git a/overlay/tio/tun_linux_offload_test.go b/overlay/tio/tun_linux_offload_test.go index 59d88cca..7535b8f3 100644 --- a/overlay/tio/tun_linux_offload_test.go +++ b/overlay/tio/tun_linux_offload_test.go @@ -866,3 +866,46 @@ func TestOffloadWriteZeroLength(t *testing.T) { } } } + +// TestWriteGSOLeadingEmptyFragmentGeometry: gso_size must be derived from +// the first non-empty fragment. Deriving it from pays[0] stamped a +// superpacket header with gso_size == 0 when the leading fragment was +// empty -- the kernel rejects that with EINVAL and the burst is lost. +// Write through a pipe and decode the vnet header the kernel would see. +func TestWriteGSOLeadingEmptyFragmentGeometry(t *testing.T) { + var pfds [2]int + if err := unix.Pipe(pfds[:]); err != nil { + t.Fatalf("pipe: %v", err) + } + t.Cleanup(func() { unix.Close(pfds[0]); unix.Close(pfds[1]) }) + + o := &Offload{fd: pfds[1], gsoIovs: make([]unix.Iovec, 2, gsoMaxIovs)} + o.gsoIovs[0].Base = &o.gsoHdrBuf[0] + o.gsoIovs[0].SetLen(virtio.Size) + + ipHdr := make([]byte, 20) + ipHdr[0] = 0x45 + udpHdr := make([]byte, 8) + seg := make([]byte, 1200) + + if err := o.WriteGSO(ipHdr, udpHdr, [][]byte{{}, seg, seg}, GSOProtoUDP); err != nil { + t.Fatalf("WriteGSO with leading empty fragment: %v", err) + } + + buf := make([]byte, virtio.Size+len(ipHdr)+len(udpHdr)+2*len(seg)+64) + n, err := unix.Read(pfds[0], buf) + if err != nil { + t.Fatalf("read pipe: %v", err) + } + var vhdr virtio.Hdr + vhdr.Decode(buf[:virtio.Size]) + if vhdr.GSOType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { + t.Errorf("GSOType=%d want UDP_L4", vhdr.GSOType) + } + if vhdr.GSOSize != 1200 { + t.Errorf("GSOSize=%d want 1200 (first non-empty fragment)", vhdr.GSOSize) + } + if want := virtio.Size + len(ipHdr) + len(udpHdr) + 2*len(seg); n != want { + t.Errorf("wrote %d bytes want %d (empty fragment must not add an iovec)", n, want) + } +}