|
| 1 | +// Copyright 2018 the u-root Authors. All rights reserved |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +//go:build go1.18 && (darwin || freebsd || linux || netbsd || openbsd || dragonfly) |
| 6 | + |
| 7 | +package nclient4 |
| 8 | + |
| 9 | +import ( |
| 10 | + "bytes" |
| 11 | + "encoding/binary" |
| 12 | + "io" |
| 13 | + "net" |
| 14 | + "testing" |
| 15 | + "time" |
| 16 | +) |
| 17 | + |
| 18 | +// fuzzFrame builds an IPv4+UDP frame with the given IPv4 total-length field and |
| 19 | +// dhcp payload. It is kept here, not shared with the other tests, so the target |
| 20 | +// builds standalone under OSS-Fuzz's go-118-fuzz-build (which sees only this file). |
| 21 | +func fuzzFrame(ipTotalLen int, dhcp []byte) []byte { |
| 22 | + pkt := make([]byte, 28+len(dhcp)) |
| 23 | + pkt[0] = 0x45 // IPv4, IHL 5 |
| 24 | + binary.BigEndian.PutUint16(pkt[2:], uint16(ipTotalLen)) |
| 25 | + pkt[9] = byte(udpProtocolNumber) |
| 26 | + binary.BigEndian.PutUint16(pkt[22:], 68) // UDP destination port |
| 27 | + binary.BigEndian.PutUint16(pkt[24:], uint16(8+len(dhcp))) |
| 28 | + copy(pkt[28:], dhcp) |
| 29 | + return pkt |
| 30 | +} |
| 31 | + |
| 32 | +// fuzzConn hands ReadFrom the fuzz frame once, then reports EOF so the receive |
| 33 | +// loop stops. |
| 34 | +type fuzzConn struct { |
| 35 | + frame []byte |
| 36 | + done bool |
| 37 | +} |
| 38 | + |
| 39 | +func (c *fuzzConn) ReadFrom(p []byte) (int, net.Addr, error) { |
| 40 | + if c.done { |
| 41 | + return 0, nil, io.EOF |
| 42 | + } |
| 43 | + c.done = true |
| 44 | + return copy(p, c.frame), &net.UDPAddr{}, nil |
| 45 | +} |
| 46 | +func (c *fuzzConn) WriteTo([]byte, net.Addr) (int, error) { return 0, nil } |
| 47 | +func (c *fuzzConn) Close() error { return nil } |
| 48 | +func (c *fuzzConn) LocalAddr() net.Addr { return &net.UDPAddr{} } |
| 49 | +func (c *fuzzConn) SetDeadline(time.Time) error { return nil } |
| 50 | +func (c *fuzzConn) SetReadDeadline(time.Time) error { return nil } |
| 51 | +func (c *fuzzConn) SetWriteDeadline(time.Time) error { return nil } |
| 52 | + |
| 53 | +// expectedReadFrom independently recomputes what ReadFrom must return for the |
| 54 | +// bytes the socket actually delivered, so the fuzz target pins the exact length, |
| 55 | +// payload, and source rather than only the frame type. It encodes the base |
| 56 | +// receive-path contract (#583, #292): the payload is the IPv4 payload after the |
| 57 | +// UDP header bounded by IP Total Length, so bytes past Total Length are dropped. |
| 58 | +// |
| 59 | +// It does not yet model the #589 UDP Length bound or the #591 fragment rejection, |
| 60 | +// which live on separate branches. When this target is rebased onto them, also |
| 61 | +// require 8 <= UDP Length <= IP payload, take the payload as |
| 62 | +// delivered[hlen+8 : hlen+udpLen], and reject MF or a non-zero fragment offset. |
| 63 | +func expectedReadFrom(delivered []byte, bufLen int) (accept bool, wantN int, payload []byte, srcIP net.IP, srcPort int) { |
| 64 | + if len(delivered) < ipv4MinimumSize { |
| 65 | + return false, 0, nil, nil, 0 |
| 66 | + } |
| 67 | + hlen := int(delivered[0]&0x0f) * 4 |
| 68 | + tlen := int(binary.BigEndian.Uint16(delivered[2:4])) |
| 69 | + // isValid: header within [20, total length], total length within the frame. |
| 70 | + if hlen < ipv4MinimumSize || hlen > tlen || tlen > len(delivered) { |
| 71 | + return false, 0, nil, nil, 0 |
| 72 | + } |
| 73 | + if delivered[0]>>4 != 4 { // IPv4 version |
| 74 | + return false, 0, nil, nil, 0 |
| 75 | + } |
| 76 | + if delivered[9] != byte(udpProtocolNumber) { |
| 77 | + return false, 0, nil, nil, 0 |
| 78 | + } |
| 79 | + ipPayloadLen := tlen - hlen |
| 80 | + if ipPayloadLen < udpMinimumSize { // #583: room for a UDP header |
| 81 | + return false, 0, nil, nil, 0 |
| 82 | + } |
| 83 | + // tlen <= len(delivered) and ipPayloadLen >= 8 together put the UDP header |
| 84 | + // and the payload up to Total Length within the delivered bytes. |
| 85 | + if binary.BigEndian.Uint16(delivered[hlen+2:hlen+4]) != 68 { // destination port |
| 86 | + return false, 0, nil, nil, 0 |
| 87 | + } |
| 88 | + payload = delivered[hlen+udpMinimumSize : tlen] // bytes past Total Length dropped (#292) |
| 89 | + wantN = ipPayloadLen - udpMinimumSize |
| 90 | + if wantN > bufLen { |
| 91 | + wantN = bufLen |
| 92 | + } |
| 93 | + srcIP = net.IP(delivered[srcAddr : srcAddr+ipv4AddressSize]) |
| 94 | + srcPort = int(binary.BigEndian.Uint16(delivered[hlen : hlen+2])) |
| 95 | + return true, wantN, payload, srcIP, srcPort |
| 96 | +} |
| 97 | + |
| 98 | +// FuzzBroadcastRawUDPConnReadFrom feeds arbitrary bytes as one raw frame into |
| 99 | +// ReadFrom with a fuzzed buffer size and checks the result against an |
| 100 | +// independent model: a rejected frame must surface as an error, and an accepted |
| 101 | +// frame must return exactly the payload, length, and source the model derives. |
| 102 | +func FuzzBroadcastRawUDPConnReadFrom(f *testing.F) { |
| 103 | + dhcp := []byte("hello world dhcp payload") |
| 104 | + seeds := [][]byte{ |
| 105 | + fuzzFrame(20, nil), // IPv4 payload 0, below the UDP header (#583) |
| 106 | + fuzzFrame(27, nil), // IPv4 payload 7, below the UDP header (#583) |
| 107 | + fuzzFrame(28, []byte{1, 2, 3}), // IPv4 payload 8, empty datagram |
| 108 | + fuzzFrame(28+len(dhcp), dhcp), // a valid reply |
| 109 | + fuzzFrame(28, []byte("trailing data past total")), // total length stops at the header (#292) |
| 110 | + {0x45, 0, 0, 40}, // header claims 40 bytes, frame truncated (#455) |
| 111 | + {0x4f, 0, 0, 60}, // IHL 15 header with nothing after it (#507) |
| 112 | + {0x45}, // truncated IPv4 header |
| 113 | + {}, // empty frame |
| 114 | + } |
| 115 | + for _, s := range seeds { |
| 116 | + f.Add(s, uint16(512)) |
| 117 | + } |
| 118 | + f.Add(fuzzFrame(28+len(dhcp), dhcp), uint16(0)) // a valid reply into an empty buffer |
| 119 | + f.Add(fuzzFrame(28+len(dhcp), dhcp), uint16(4)) // ... and one too small to hold it |
| 120 | + |
| 121 | + f.Fuzz(func(t *testing.T, raw []byte, bufLen uint16) { |
| 122 | + b := make([]byte, int(bufLen%2048)) |
| 123 | + upc := &BroadcastRawUDPConn{ |
| 124 | + PacketConn: &fuzzConn{frame: raw}, |
| 125 | + boundAddr: &net.UDPAddr{Port: 68}, |
| 126 | + } |
| 127 | + n, addr, err := upc.ReadFrom(b) |
| 128 | + |
| 129 | + // The socket copies raw into a fixed-size receive buffer, so ReadFrom only |
| 130 | + // ever parses this prefix. |
| 131 | + maxRecv := ipv4MaximumHeaderSize + udpMinimumSize + len(b) |
| 132 | + delivered := raw |
| 133 | + if len(delivered) > maxRecv { |
| 134 | + delivered = delivered[:maxRecv] |
| 135 | + } |
| 136 | + accept, wantN, payload, srcIP, srcPort := expectedReadFrom(delivered, len(b)) |
| 137 | + |
| 138 | + if !accept { |
| 139 | + if err == nil { |
| 140 | + t.Fatalf("accepted a frame the model rejects: n=%d delivered=%x", n, delivered) |
| 141 | + } |
| 142 | + return |
| 143 | + } |
| 144 | + if err != nil { |
| 145 | + t.Fatalf("rejected a frame the model accepts: err=%v delivered=%x", err, delivered) |
| 146 | + } |
| 147 | + if n != wantN || n < 0 || n > len(b) { |
| 148 | + t.Fatalf("ReadFrom n=%d, want %d (buffer %d)", n, wantN, len(b)) |
| 149 | + } |
| 150 | + if !bytes.Equal(b[:n], payload[:wantN]) { |
| 151 | + t.Fatalf("ReadFrom payload=%x, want %x", b[:n], payload[:wantN]) |
| 152 | + } |
| 153 | + ua, ok := addr.(*net.UDPAddr) |
| 154 | + if !ok || ua == nil { |
| 155 | + t.Fatalf("ReadFrom addr=%v, want *net.UDPAddr", addr) |
| 156 | + } |
| 157 | + if !ua.IP.Equal(srcIP) { |
| 158 | + t.Fatalf("ReadFrom source IP=%v, want %v", ua.IP, srcIP) |
| 159 | + } |
| 160 | + if ua.Port != srcPort { |
| 161 | + t.Fatalf("ReadFrom source port=%d, want %d", ua.Port, srcPort) |
| 162 | + } |
| 163 | + }) |
| 164 | +} |
0 commit comments