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) + } +}