mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 09:36:58 +02:00
tio.Offload.WriteGSO: reject 0-len packets, check seg lengths
This commit is contained in:
+1
-1
@@ -115,7 +115,7 @@ const (
|
||||
// (mutable: the L4 checksum field must hold the pseudo-header partial, single-fold not inverted, per virtio NEEDS_CSUM semantics).
|
||||
// pays are non-overlapping payload fragments whose concatenation is the full superpacket payload.
|
||||
// They are read-only from the writer's perspective and must remain valid until the call returns.
|
||||
// Every segment in pays except possibly the last is exactly the same size.
|
||||
// Every segment in pays except possibly the last must be exactly the same size.
|
||||
// proto picks the L4 protocol so the writer knows which gsoType / CsumOffset to set.
|
||||
//
|
||||
// Callers should also consult CapsProvider (via SupportsGSO) for the per-protocol negotiated capability:
|
||||
|
||||
@@ -327,52 +327,45 @@ func (r *Offload) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte, proto
|
||||
r.gsoIovs[2].Base = &transportHdr[0]
|
||||
r.gsoIovs[2].SetLen(len(transportHdr))
|
||||
|
||||
// Fill out the payload iovecs and find the GSO geometry:
|
||||
segSize := 0
|
||||
segSize := len(pays[0])
|
||||
total := len(hdr) + len(transportHdr)
|
||||
n := 3
|
||||
for _, p := range pays {
|
||||
total += len(p)
|
||||
for i, p := range pays {
|
||||
if len(p) == 0 {
|
||||
continue //disregard empty payloads, the kernel will reject them.
|
||||
//callers already funnel zero-length frames via the not-GSO path, so this should never happen.
|
||||
// The coalescers route zero-payload packets down the non-GSO path,
|
||||
// so an empty fragment means the caller's accounting is broken.
|
||||
return fmt.Errorf("tio: WriteGSO empty payload fragment %d of %d", i, len(pays))
|
||||
} else if len(p) > segSize || (len(p) < segSize && i != len(pays)-1) {
|
||||
// all segments must be the same size, except for the last one
|
||||
return fmt.Errorf("tio: WriteGSO fragment %d is %dB, want %dB segments (only the last may be shorter)", i, len(p), segSize)
|
||||
}
|
||||
if n == 3 {
|
||||
segSize = len(p)
|
||||
}
|
||||
r.gsoIovs[n].Base = &p[0]
|
||||
r.gsoIovs[n].SetLen(len(p))
|
||||
n++
|
||||
total += len(p)
|
||||
r.gsoIovs[3+i].Base = &p[0]
|
||||
r.gsoIovs[3+i].SetLen(len(p))
|
||||
}
|
||||
r.gsoIovs = r.gsoIovs[:n]
|
||||
segCount := n - 3
|
||||
// This check keeps `total` in the uint16 range. Anything larger would wrap around and cause trouble.
|
||||
if total > maxSuperpacketLen {
|
||||
return fmt.Errorf("tio: WriteGSO superpacket %dB exceeds %d", total, maxSuperpacketLen)
|
||||
}
|
||||
// gsoType and GSOSize stay zero (GSO_NONE, 0) for single-segment, or an unknown IP version.
|
||||
|
||||
// A single segment ships as a plain checksummed packet (GSO_NONE, size 0).
|
||||
// Multiple segments carry the real GSO type and segSize, which the loop
|
||||
// above verified is the size of every fragment except possibly the last.
|
||||
gsoType := uint8(unix.VIRTIO_NET_HDR_GSO_NONE)
|
||||
if len(pays) > 1 {
|
||||
gsoType = gsoTypeFromProto(proto, hdr[0]>>4)
|
||||
}
|
||||
var gsoSize uint16
|
||||
if gsoType != unix.VIRTIO_NET_HDR_GSO_NONE {
|
||||
gsoSize = uint16(segSize)
|
||||
}
|
||||
vhdr := virtio.NewHeader(
|
||||
unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/
|
||||
unix.VIRTIO_NET_HDR_GSO_NONE, /*gsoType*/
|
||||
gsoType, /*gsoType*/
|
||||
uint16(len(hdr)+len(transportHdr)), /*hdrLen*/
|
||||
0, /*gsoSize*/
|
||||
gsoSize, /*gsoSize*/
|
||||
uint16(len(hdr)), /*csumStart*/
|
||||
csumOff, /*csumOffset*/
|
||||
)
|
||||
if segCount > 1 {
|
||||
ipVer := hdr[0] >> 4
|
||||
switch {
|
||||
case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6):
|
||||
vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_UDP_L4)
|
||||
case ipVer == 6:
|
||||
vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_TCPV6)
|
||||
case ipVer == 4:
|
||||
vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_TCPV4)
|
||||
}
|
||||
if vhdr.GSOType() != unix.VIRTIO_NET_HDR_GSO_NONE {
|
||||
vhdr.GSOSize = uint16(segSize)
|
||||
}
|
||||
}
|
||||
vhdr.Encode(r.gsoHdrBuf[:])
|
||||
|
||||
_, err := r.rawWrite(r.gsoIovs)
|
||||
|
||||
@@ -25,6 +25,20 @@ func protoFromGSOType(t uint8) (GSOProto, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// gsoTypeFromProto is the reverse of protoFromGSOType
|
||||
func gsoTypeFromProto(proto GSOProto, ipVer uint8) uint8 {
|
||||
switch {
|
||||
case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6):
|
||||
return unix.VIRTIO_NET_HDR_GSO_UDP_L4
|
||||
case ipVer == 6:
|
||||
return unix.VIRTIO_NET_HDR_GSO_TCPV6
|
||||
case ipVer == 4:
|
||||
return unix.VIRTIO_NET_HDR_GSO_TCPV4
|
||||
default:
|
||||
return unix.VIRTIO_NET_HDR_GSO_NONE
|
||||
}
|
||||
}
|
||||
|
||||
// SegmentSuperpacket invokes fn once per segment of pkt.
|
||||
// For non-GSO pkts fn is called once with pkt.Bytes.
|
||||
// For GSO/USO superpackets, fn is called once per segment with a slice of pkt.Bytes holding that segment's plaintext
|
||||
|
||||
@@ -744,38 +744,6 @@ func TestSegmentSuperpacketNoAlloc(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteGSOSkipsEmptyPayloads is the defense-in-depth guard for the
|
||||
// zero-length UDP DoS: a payload fragment of length zero would make &p[0]
|
||||
// panic (index-out-of-range) when building the iovec array. WriteGSO must
|
||||
// skip empties instead. We write to /dev/null so the writev always succeeds
|
||||
// synchronously; the point is simply that neither call panics.
|
||||
func TestWriteGSOSkipsEmptyPayloads(t *testing.T) {
|
||||
fd, err := unix.Open("/dev/null", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("open /dev/null: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = unix.Close(fd) })
|
||||
|
||||
o := &Offload{fd: fd, 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 // IPv4, IHL 5
|
||||
udpHdr := make([]byte, 8)
|
||||
|
||||
// Sole payload empty: exercises the all-empty skip (n stays at 3).
|
||||
if err := o.WriteGSO(ipHdr, udpHdr, [][]byte{{}}, GSOProtoUDP); err != nil {
|
||||
t.Fatalf("WriteGSO with a single empty payload: %v", err)
|
||||
}
|
||||
// Empty mixed with a real fragment: exercises the index-drift skip so a
|
||||
// later non-empty payload still lands in the right iovec slot.
|
||||
real := make([]byte, 1200)
|
||||
if err := o.WriteGSO(ipHdr, udpHdr, [][]byte{real, {}}, GSOProtoUDP); err != nil {
|
||||
t.Fatalf("WriteGSO with a trailing empty payload: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildTSOv6 builds a synthetic IPv6/TCP TSO superpacket with payLen bytes
|
||||
// of payload, segmented at gso. Returns the packet bytes only; the
|
||||
// virtio_net_hdr is the caller's responsibility.
|
||||
@@ -943,12 +911,12 @@ 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) {
|
||||
// TestWriteGSOSuperpacketGeometry decodes the vnet header the kernel would see for a multi-segment write:
|
||||
// the GSO type must match the proto and IP version
|
||||
// gso_size must be the per-segment size (the kernel rejects a superpacket with gso_size == 0),
|
||||
// and the csum fields must point at the transport header's checksum slot.
|
||||
// Write through a pipe so the bytes can be read back and decoded.
|
||||
func TestWriteGSOSuperpacketGeometry(t *testing.T) {
|
||||
var pfds [2]int
|
||||
if err := unix.Pipe(pfds[:]); err != nil {
|
||||
t.Fatalf("pipe: %v", err)
|
||||
@@ -964,8 +932,8 @@ func TestWriteGSOLeadingEmptyFragmentGeometry(t *testing.T) {
|
||||
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)
|
||||
if err := o.WriteGSO(ipHdr, udpHdr, [][]byte{seg, seg}, GSOProtoUDP); err != nil {
|
||||
t.Fatalf("WriteGSO: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, virtio.Size+len(ipHdr)+len(udpHdr)+2*len(seg)+64)
|
||||
@@ -979,18 +947,20 @@ func TestWriteGSOLeadingEmptyFragmentGeometry(t *testing.T) {
|
||||
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)
|
||||
t.Errorf("GSOSize=%d want 1200 (per-segment size from pays[0])", vhdr.GSOSize)
|
||||
}
|
||||
if vhdr.HdrLen != uint16(len(ipHdr)+len(udpHdr)) {
|
||||
t.Errorf("HdrLen=%d want %d", vhdr.HdrLen, len(ipHdr)+len(udpHdr))
|
||||
}
|
||||
if vhdr.CsumStart != uint16(len(ipHdr)) || vhdr.CsumOffset != 6 {
|
||||
t.Errorf("csum start/offset = %d/%d want %d/6", vhdr.CsumStart, vhdr.CsumOffset, len(ipHdr))
|
||||
}
|
||||
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)
|
||||
t.Errorf("wrote %d bytes want %d", n, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteGSORejectsBadGeometry pins the length-check contract: malformed
|
||||
// geometry must fail loudly instead of silently succeeding (the old empty-
|
||||
// header early-out returned nil and dropped the payload), and nothing may
|
||||
// reach the u16 virtio fields or the kernel's csum_start+csum_offset write
|
||||
// without covering them.
|
||||
// TestWriteGSORejectsBadGeometry pins the length-check contracts
|
||||
func TestWriteGSORejectsBadGeometry(t *testing.T) {
|
||||
fd, err := unix.Open("/dev/null", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
@@ -1019,6 +989,13 @@ func TestWriteGSORejectsBadGeometry(t *testing.T) {
|
||||
{"udp-transport-too-short-for-csum", ipHdr, udpHdr[:6], [][]byte{seg}, GSOProtoUDP, true},
|
||||
{"tcp-transport-too-short-for-csum", ipHdr, tcpHdr[:16], [][]byte{seg}, GSOProtoTCP, true},
|
||||
{"superpacket-over-65535", ipHdr, tcpHdr, [][]byte{make([]byte, 40000), make([]byte, 40000)}, GSOProtoTCP, true},
|
||||
{"sole-payload-empty", ipHdr, udpHdr, [][]byte{{}}, GSOProtoUDP, true},
|
||||
{"leading-empty-fragment", ipHdr, udpHdr, [][]byte{{}, seg, seg}, GSOProtoUDP, true},
|
||||
{"trailing-empty-fragment", ipHdr, tcpHdr, [][]byte{seg, {}}, GSOProtoTCP, true},
|
||||
{"oversize-middle-fragment", ipHdr, udpHdr, [][]byte{seg, make([]byte, 1201), seg}, GSOProtoUDP, true},
|
||||
{"undersize-middle-fragment", ipHdr, udpHdr, [][]byte{seg, make([]byte, 100), seg}, GSOProtoUDP, true},
|
||||
{"oversize-last-fragment", ipHdr, tcpHdr, [][]byte{seg, make([]byte, 1201)}, GSOProtoTCP, true},
|
||||
{"short-last-fragment-ok", ipHdr, udpHdr, [][]byte{seg, seg, make([]byte, 100)}, GSOProtoUDP, false},
|
||||
{"no-pays-noop", ipHdr, udpHdr, nil, GSOProtoUDP, false},
|
||||
{"valid-udp", ipHdr, udpHdr, [][]byte{seg, seg}, GSOProtoUDP, false},
|
||||
{"valid-tcp", ipHdr, tcpHdr, [][]byte{seg, seg}, GSOProtoTCP, false},
|
||||
|
||||
Reference in New Issue
Block a user