Files
Mike Jensen 4b2cd69d7c fuzz: expand fuzzing test coverage (#21123)
This change expands our fuzzing coverage in protocol and parsing logic. No issues discovered from this fuzzing. Wiring into oss-fuzz for continual coverage.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: I6b5218cb1103ccc5b957c512a10d87f637c4b6e5

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-14 09:07:29 -06:00

309 lines
14 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"testing"
"tailscale.com/types/ipproto"
)
func FuzzParsedDecode(f *testing.F) {
f.Add([]byte{0x45, 0x00, 0x00, 0x14})
f.Add([]byte{0x60, 0x00, 0x12, 0x34, 0x56, 0x78})
f.Add([]byte{0x44, 0x00, 0x01, 0x02})
// A complete minimal IPv4 UDP packet (20-byte header + 8-byte UDP header)
f.Add([]byte{
0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x40, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00,
})
// A complete minimal IPv6 UDP packet (40-byte header + 8-byte UDP header)
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x08, byte(ipproto.UDP), 0x40,
}, make([]byte, 32)...), 0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00))
// A complete IPv4 ICMP packet
f.Add([]byte{
0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.ICMPv4),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})
// A complete IPv6 ICMPv6 packet
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x04, byte(ipproto.ICMPv6), 0x40,
}, make([]byte, 32)...), 0x80, 0x00, 0x00, 0x00))
// A TSMP packet (Tailscale's inter-node message protocol)
f.Add([]byte{
0x45, 0x00, 0x00, 0x2b, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00,
})
// A later (non-first) IPv4 fragment with a high offset
f.Add([]byte{
0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x20, 0x10, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})
// Empty input
f.Add([]byte{})
// A bad IP version nibble, hitting the default (unknown) case
f.Add([]byte{0x50, 0x00, 0x00, 0x14, 0x12, 0x34, 0x00, 0x00, 0x40, 0x11,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// IPv4 declared length (0x0100) larger than the buffer
f.Add([]byte{0x45, 0x00, 0x01, 0x00, 0x12, 0x34, 0x00, 0x00, 0x40, 0x11,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// IHL 15: sub-proto offset (60) beyond the packet end
f.Add([]byte{0x4f, 0x00, 0x00, 0x14, 0x12, 0x34, 0x00, 0x00, 0x40, 0x11,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// IHL 6: 4 option bytes, UDP sub-header at offset 24
f.Add([]byte{0x46, 0x00, 0x00, 0x24, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x01, 0x02, 0x03, 0x04,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00})
// IHL 3: sub-header read from inside the IP addresses (IHL < 5 is not validated)
f.Add([]byte{0x43, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
// Truncated sub-headers: TCP, UDP, ICMPv4, IGMP, SCTP
f.Add([]byte{0x45, 0x00, 0x00, 0x18, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39, 0x04, 0xd2})
f.Add([]byte{0x45, 0x00, 0x00, 0x16, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39})
f.Add([]byte{0x45, 0x00, 0x00, 0x16, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.ICMPv4),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x08, 0x00})
f.Add([]byte{0x45, 0x00, 0x00, 0x18, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.IGMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x11, 0x64, 0x00, 0x00})
f.Add([]byte{0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.SCTP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01})
// A fragmented TSMP, strictly disallowed
f.Add([]byte{0x45, 0x00, 0x00, 0x2b, 0x12, 0x34, 0x20, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00})
// A TSMP sub-header shorter than the minimum 7 bytes
f.Add([]byte{0x45, 0x00, 0x00, 0x26, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60})
// A later fragment with an offset below minFragBlks (8 < 10), rejected
f.Add([]byte{0x45, 0x00, 0x00, 0x14, 0x12, 0x34, 0x20, 0x08, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// A protocol byte of 0xff, the internal Fragment sentinel
f.Add([]byte{0x45, 0x00, 0x00, 0x14, 0x12, 0x34, 0x00, 0x00, 0x40, 0xff,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// IPv6 declared payload length (16) larger than the buffer provides
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x10, byte(ipproto.UDP), 0x40},
make([]byte, 32)...), 0x30))
// A truncated IPv6 fragment header (payload length 4, frag header cut off)
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x04, byte(ip6FragHeader), 0x40},
make([]byte, 32)...), 0x11, 0x00, 0x00, 0x00))
// IPv6 with truncated sub-headers: ICMPv6, UDP, TCP
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x02, byte(ipproto.ICMPv6), 0x40},
make([]byte, 32)...), 0x80, 0x00))
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x04, byte(ipproto.UDP), 0x40},
make([]byte, 32)...), 0x30, 0x39, 0x04, 0xd2))
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x08, byte(ipproto.TCP), 0x40},
make([]byte, 32)...), 0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01))
// IPv6 with the internal Fragment sentinel as the next-header byte
f.Add(append(append([]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x04, 0xff, 0x40},
make([]byte, 32)...), 0x00, 0x00, 0x00, 0x00))
f.Fuzz(func(t *testing.T, b []byte) {
var q Parsed
q.Decode(b)
})
}
func FuzzDecode4(f *testing.F) {
f.Add(make([]byte, ip4HeaderLength))
f.Add([]byte{0x45, 0x00, 0x04, 0x1e, 0x12, 0x34, 0x40, 0x00, 0x80, 0x11})
// A complete minimal IPv4 TCP packet (20-byte header + 20-byte TCP header)
f.Add([]byte{
0x45, 0x00, 0x00, 0x28, 0x12, 0x34, 0x40, 0x00, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x50, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
})
// A complete minimal IPv4 UDP packet
f.Add([]byte{
0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x40, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00,
})
// A complete IPv4 ICMP packet
f.Add([]byte{
0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.ICMPv4),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})
// A complete IPv4 IGMP packet (8-byte IGMP header)
f.Add([]byte{
0x45, 0x00, 0x00, 0x20, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.IGMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x11, 0x64, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x01,
})
// A complete IPv4 SCTP packet (12-byte SCTP header)
f.Add([]byte{
0x45, 0x00, 0x00, 0x24, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.SCTP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
})
// A TSMP packet
f.Add([]byte{
0x45, 0x00, 0x00, 0x2b, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00,
})
// IHL 15: sub-proto offset (60) beyond the packet end
f.Add([]byte{0x4f, 0x00, 0x00, 0x14, 0x12, 0x34, 0x00, 0x00, 0x40, 0x11,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// IHL 6: 4 option bytes, UDP sub-header at offset 24
f.Add([]byte{0x46, 0x00, 0x00, 0x24, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x01, 0x02, 0x03, 0x04,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00})
// Truncated sub-headers: TCP, UDP, ICMPv4, IGMP, SCTP
f.Add([]byte{0x45, 0x00, 0x00, 0x18, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39, 0x04, 0xd2})
f.Add([]byte{0x45, 0x00, 0x00, 0x16, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.UDP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39})
f.Add([]byte{0x45, 0x00, 0x00, 0x16, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.ICMPv4),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x08, 0x00})
f.Add([]byte{0x45, 0x00, 0x00, 0x18, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.IGMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x11, 0x64, 0x00, 0x00})
f.Add([]byte{0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.SCTP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01})
// A fragmented TSMP, strictly disallowed
f.Add([]byte{0x45, 0x00, 0x00, 0x2b, 0x12, 0x34, 0x20, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2,
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00})
// A TSMP sub-header shorter than the minimum 7 bytes
f.Add([]byte{0x45, 0x00, 0x00, 0x26, 0x12, 0x34, 0x00, 0x00, 0x40, byte(ipproto.TSMP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60})
// A later fragment with an offset below minFragBlks (8 < 10), rejected
f.Add([]byte{0x45, 0x00, 0x00, 0x14, 0x12, 0x34, 0x20, 0x08, 0x40, byte(ipproto.TCP),
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// A protocol byte of 0xff, the internal Fragment sentinel
f.Add([]byte{0x45, 0x00, 0x00, 0x14, 0x12, 0x34, 0x00, 0x00, 0x40, 0xff,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
f.Fuzz(func(t *testing.T, b []byte) {
var q Parsed
q.decode4(b)
})
}
func FuzzDecode6(f *testing.F) {
f.Add(make([]byte, ip6HeaderLength))
f.Add(append(make([]byte, 40), 0x2c)) // Next Header = Fragment (44)
// A complete minimal IPv6 UDP packet (40-byte header + 8-byte UDP header)
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x08, byte(ipproto.UDP), 0x40,
}, make([]byte, 32)...), 0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00))
// A first fragment: base header Next Header 44, then a fragment header
// with offset 0 and the real Next Header (UDP), then a UDP header
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x10, byte(ip6FragHeader), 0x40,
}, make([]byte, 32)...),
byte(ipproto.UDP), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x30, 0x39, 0x04, 0xd2, 0x00, 0x08, 0x00, 0x00))
// A complete minimal IPv6 TCP packet (40-byte header + 20-byte TCP header)
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x14, byte(ipproto.TCP), 0x40,
}, make([]byte, 32)...),
0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x50, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00))
// A complete IPv6 ICMPv6 packet
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x04, byte(ipproto.ICMPv6), 0x40,
}, make([]byte, 32)...), 0x80, 0x00, 0x00, 0x00))
// A complete IPv6 SCTP packet (12-byte SCTP header)
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x0c, byte(ipproto.SCTP), 0x40,
}, make([]byte, 32)...),
0x30, 0x39, 0x04, 0xd2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00))
// A TSMP packet
f.Add(append(append([]byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x07, byte(ipproto.TSMP), 0x40,
}, make([]byte, 32)...), 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70))
f.Fuzz(func(t *testing.T, b []byte) {
var q Parsed
q.decode6(b)
})
}
func FuzzDecode6Fragment(f *testing.F) {
// A fragment header: next-header(1), reserved(1), frag-ofs+flags(2), identification(4)
f.Add([]byte{0x11, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
f.Add(make([]byte, ip6FragHeaderLength))
// A later fragment with a non-zero offset (passed through as ipproto.Fragment)
f.Add([]byte{byte(ipproto.UDP), 0x00, 0x01, 0x00, 0x03, 0x04, 0x05, 0x06, 0xaa, 0xbb})
// 40 dummy bytes + an 8-byte frag header: first fragment (offset 0, next=UDP)
f.Add(append(make([]byte, 40), byte(ipproto.UDP), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01))
// Later fragment with offset 1 (< minFragBlks), rejected
f.Add(append(make([]byte, 40), byte(ipproto.UDP), 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01))
// Later fragment with offset 32, passed through as ipproto.Fragment
f.Add(append(make([]byte, 40), byte(ipproto.UDP), 0x00, 0x01, 0x00, 0x03, 0x04, 0x05, 0x06))
f.Fuzz(func(t *testing.T, b []byte) {
var q Parsed
q.subofs = 40
q.decode6Fragment(b)
})
}
func FuzzIP4Checksum(f *testing.F) {
f.Add([]byte{0x45, 0x00, 0x04, 0x1e})
// A full 20-byte IPv4 header
f.Add([]byte{0x45, 0x00, 0x00, 0x14, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11,
0x00, 0x00, 10, 0, 0, 1, 10, 0, 0, 2})
// An odd-length input to exercise the trailing-byte path
f.Add([]byte{0x45, 0x00, 0x04, 0x1e, 0xab})
// An empty header, skipping the loop entirely
f.Add([]byte{})
f.Fuzz(func(t *testing.T, b []byte) {
_ = ip4Checksum(b)
})
}
func FuzzChecksumBytes(f *testing.F) {
f.Add([]byte{0x12, 0x34, 0x56}, uint16(0))
f.Add([]byte{}, uint16(1))
// All-ones inputs to exercise the checksum fold/carry path
f.Add([]byte{0xff, 0xff, 0xff, 0xff}, uint16(0xffff))
f.Add([]byte{0x80}, uint16(0x8000))
f.Fuzz(func(t *testing.T, b []byte, initial uint16) {
_ = checksumBytes(b, initial)
})
}
func FuzzICMP6Checksum(f *testing.F) {
var src, dst [16]byte
f.Add([]byte{0x80, 0x00, 0x12, 0x34}, []byte("ping"))
// A real 8-byte ICMPv6 echo header with a zeroed checksum field
f.Add([]byte{0x80, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x01}, []byte("payload"))
// Partially-copied headers and empty payload/header
f.Add([]byte{0x80}, []byte("payload"))
f.Add([]byte{}, []byte("payload"))
f.Add([]byte{0x80, 0x00, 0x12, 0x34}, []byte{})
// Variable-length header and payload
f.Fuzz(func(t *testing.T, header, payload []byte) {
_ = icmp6Checksum(header, src, dst, payload)
})
}
func FuzzGeneveDecode(f *testing.F) {
f.Add([]byte{0x00, 0x40, 0x65, 0xb8, 0x00, 0x01, 0x02, 0x03})
f.Add(make([]byte, GeneveFixedHeaderLength))
// A full valid Geneve header: v0, options-length 2 (8 option bytes)
f.Add(append([]byte{0x00, 0x40, 0x65, 0xb8, 0x12, 0x34, 0x56, 0x00},
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08))
// One byte short of the fixed header
f.Add([]byte{0x00, 0x40, 0x65, 0xb8, 0x00, 0x01, 0x02})
// Control bit set, and a non-zero version nibble
f.Add([]byte{0x00, 0xc0, 0x65, 0xb8, 0x12, 0x34, 0x56, 0x00})
f.Add([]byte{0xc0, 0x40, 0x65, 0xb8, 0x12, 0x34, 0x56, 0x00})
f.Fuzz(func(t *testing.T, b []byte) {
var h GeneveHeader
_ = h.Decode(b)
})
}