/src/tcpreplay/test/fuzz/fuzz_pcap.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2026 Fred Klassen <tcpreplay.dev at gmail dot com> - AppNeta by Broadcom |
3 | | * |
4 | | * The Tcpreplay Suite of tools is free software: you can redistribute it |
5 | | * and/or modify it under the terms of the GNU General Public License as |
6 | | * published by the Free Software Foundation, either version 3 of the |
7 | | * License, or with the authors permission any later version. |
8 | | * |
9 | | * The Tcpreplay Suite is distributed in the hope that it will be useful, |
10 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | | * GNU General Public License for more details. |
13 | | * |
14 | | * You should have received a copy of the GNU General Public License |
15 | | * along with the Tcpreplay Suite. If not, see <http://www.gnu.org/licenses/>. |
16 | | */ |
17 | | |
18 | | /* |
19 | | * Fuzz target: the packet header parsers, driven from a whole pcap file. |
20 | | * |
21 | | * This is the surface that reads untrusted input in every tool - tcpprep, |
22 | | * tcprewrite, tcpreplay and tcpcapinfo all start by opening a capture |
23 | | * somebody else produced - and it is where most of the 2026 advisories |
24 | | * landed: |
25 | | * |
26 | | * GHSA-jj65-mrgg-f5fx heap over-read walking IPv6 extension headers |
27 | | * (get_layer4_v6); reported against 4.4.4, marked |
28 | | * patched, still reproducing on 4.5.4 |
29 | | * GHSA-5q26-7fxx-v8fh heap OOB in ARP rewriting, from trusting the |
30 | | * packet's own ar_hln/ar_pln |
31 | | * GHSA-m6w7-8497-g9c9 heap over-read via --pktlen, where the on-the-wire |
32 | | * length exceeds what was captured |
33 | | * GHSA-ww62-mxv7-pg55 SEGV in the DLT_JUNIPER_ETHER decoder |
34 | | * |
35 | | * The fuzzer drives the pcap through libpcap exactly as the tools do, then |
36 | | * runs each packet through the shared header parsers. The DLT comes from the |
37 | | * file, so the input controls which decode path is taken and the whole DLT |
38 | | * plugin matrix is reachable from one target. |
39 | | * |
40 | | * The capture is fed through fmemopen() rather than a temp file: this runs |
41 | | * millions of times and the filesystem would dominate. |
42 | | */ |
43 | | |
44 | | #include "fuzz_common.h" |
45 | | |
46 | | #include "common/get.h" |
47 | | #include "defines.h" |
48 | | |
49 | | #include <pcap.h> |
50 | | |
51 | | int |
52 | | LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
53 | 1.45k | { |
54 | 1.45k | FILE *fp; |
55 | 1.45k | pcap_t *pcap; |
56 | 1.45k | char errbuf[PCAP_ERRBUF_SIZE]; |
57 | 1.45k | struct pcap_pkthdr *pkthdr; |
58 | 1.45k | const u_char *pktdata; |
59 | 1.45k | int datalink; |
60 | 1.45k | int rc; |
61 | 1.45k | unsigned int packets = 0; |
62 | | |
63 | | /* a pcap file header is 24 bytes; below that there is nothing to open */ |
64 | 1.45k | if (size < 24) |
65 | 8 | return 0; |
66 | | |
67 | | /* |
68 | | * fmemopen wants a writable pointer but libpcap only reads. Casting away |
69 | | * const on the fuzzer's buffer is not on - copy it. |
70 | | */ |
71 | 1.44k | void *copy = malloc(size); |
72 | 1.44k | if (copy == NULL) |
73 | 0 | return 0; |
74 | 1.44k | memcpy(copy, data, size); |
75 | | |
76 | 1.44k | fp = fmemopen(copy, size, "rb"); |
77 | 1.44k | if (fp == NULL) { |
78 | 0 | free(copy); |
79 | 0 | return 0; |
80 | 0 | } |
81 | | |
82 | 1.44k | pcap = pcap_fopen_offline(fp, errbuf); |
83 | 1.44k | if (pcap == NULL) { |
84 | | /* malformed header: libpcap rejected it, which is the correct outcome */ |
85 | 263 | fclose(fp); |
86 | 263 | free(copy); |
87 | 263 | return 0; |
88 | 263 | } |
89 | | /* from here on the pcap_t owns fp and closes it in pcap_close() - closing |
90 | | * it again here is a double free, and it is the harness that would be |
91 | | * wrong, not libpcap */ |
92 | | |
93 | 1.18k | datalink = pcap_datalink(pcap); |
94 | | |
95 | 22.6k | while ((rc = pcap_next_ex(pcap, &pkthdr, &pktdata)) == 1) { |
96 | 21.4k | uint16_t ethertype; |
97 | 21.4k | uint32_t l2len, l2offset, vlan_offset; |
98 | | |
99 | | /* |
100 | | * Cap the work per input. A crafted pcap can claim an enormous packet |
101 | | * count, and the fuzzer's time is better spent on new shapes than on |
102 | | * one pathological file. |
103 | | */ |
104 | 21.4k | if (++packets > 512) |
105 | 5 | break; |
106 | | |
107 | 21.4k | if (pkthdr->caplen == 0) |
108 | 8.63k | continue; |
109 | | |
110 | | /* |
111 | | * The parsers take an end pointer computed from caplen. Deliberately |
112 | | * use caplen, not len: the gap between them is what GHSA-m6w7-8497-g9c9 |
113 | | * was about, and a harness that papered over it would hide the bug |
114 | | * class it exists to find. |
115 | | */ |
116 | 12.7k | if (get_l2len_protocol(pktdata, |
117 | 12.7k | pkthdr->caplen, |
118 | 12.7k | datalink, |
119 | 12.7k | ðertype, |
120 | 12.7k | &l2len, |
121 | 12.7k | &l2offset, |
122 | 12.7k | &vlan_offset) < 0) |
123 | 2.10k | continue; |
124 | | |
125 | 10.6k | if (l2len > pkthdr->caplen) |
126 | 0 | continue; |
127 | | |
128 | 10.6k | switch (ethertype) { |
129 | 3.65k | case ETHERTYPE_IP: { |
130 | 3.65k | ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(pktdata + l2len); |
131 | | |
132 | 3.65k | if (pkthdr->caplen < l2len + sizeof(ipv4_hdr_t)) |
133 | 269 | break; |
134 | | |
135 | 3.38k | (void)get_layer4_v4(ip_hdr, pktdata + pkthdr->caplen - 1); |
136 | 3.38k | break; |
137 | 3.65k | } |
138 | | |
139 | 3.12k | case ETHERTYPE_IP6: { |
140 | 3.12k | ipv6_hdr_t *ip6_hdr = (ipv6_hdr_t *)(pktdata + l2len); |
141 | | |
142 | 3.12k | if (pkthdr->caplen < l2len + sizeof(ipv6_hdr_t)) |
143 | 89 | break; |
144 | | |
145 | | /* GHSA-jj65-mrgg-f5fx */ |
146 | 3.03k | (void)get_layer4_v6(ip6_hdr, pktdata + pkthdr->caplen - 1); |
147 | 3.03k | (void)get_ipv6_l4proto(ip6_hdr, pktdata + pkthdr->caplen - 1); |
148 | 3.03k | break; |
149 | 3.12k | } |
150 | | |
151 | 3.90k | default: |
152 | 3.90k | break; |
153 | 10.6k | } |
154 | 10.6k | } |
155 | | |
156 | 1.18k | pcap_close(pcap); |
157 | 1.18k | free(copy); |
158 | 1.18k | return 0; |
159 | 1.18k | } |