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 <noreply@anthropic.com>
This commit is contained in:
JackDoan
2026-07-27 15:38:21 -05:00
parent 5c0a5ee4be
commit 6c0305c7ae
2 changed files with 59 additions and 2 deletions
+16 -2
View File
@@ -334,14 +334,28 @@ func (r *Offload) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte, proto
default: default:
csumOff = 16 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{ vhdr := virtio.Hdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
HdrLen: uint16(len(hdr) + len(transportHdr)), HdrLen: uint16(len(hdr) + len(transportHdr)),
GSOSize: uint16(len(pays[0])), GSOSize: uint16(segSize),
CsumStart: uint16(len(hdr)), CsumStart: uint16(len(hdr)),
CsumOffset: csumOff, CsumOffset: csumOff,
} }
if len(pays) > 1 { if segCount > 1 {
ipVer := hdr[0] >> 4 ipVer := hdr[0] >> 4
switch { switch {
case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6): case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6):
+43
View File
@@ -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)
}
}