Line data Source code
1 : /* fuzz_quic_wire is a simple and stateless fuzz target for fd_quic.
2 :
3 : The attack surface consists of fd_quic's packet handlers.
4 : The input vectors are the raw contents of UDP datagrams (in encrypted
5 : form) A custom mutator is used to temporarily remove the decryption
6 : before calling the generic libFuzzer mutator. If we tried mutating
7 : the encrypted inputs directly, everything would just be an encryption
8 : failure.
9 :
10 : The goal of fuzz_quic_wire is to cover the early upstream stages of
11 : the QUIC packet processing pipeline. This includes packet header
12 : parsing, connection creation, retry handling, etc. */
13 :
14 : #include "../../../util/sanitize/fd_fuzz.h"
15 : #include "fd_quic_test_helpers.h"
16 : #include "../crypto/fd_quic_crypto_suites.h"
17 : #include "../templ/fd_quic_parse_util.h"
18 : #include "../../tls/test_tls_helper.h"
19 : #include "../../../util/net/fd_eth.h"
20 : #include "../../../util/net/fd_ip4.h"
21 : #include "../../../util/net/fd_udp.h"
22 : #include "../fd_quic_proto.h"
23 : #include "../fd_quic_proto.c"
24 : #include "../fd_quic_private.h"
25 :
26 : #include <assert.h>
27 :
28 : static void
29 : my_stream_new_cb( fd_quic_stream_t * stream,
30 0 : void * quic_ctx ) {
31 0 : (void)stream; (void)quic_ctx;
32 0 : }
33 :
34 : static void
35 : my_stream_notify_cb( fd_quic_stream_t * stream,
36 : void * stream_ctx,
37 0 : int notify_type ) {
38 0 : (void)stream; (void)stream_ctx; (void)notify_type;
39 0 : }
40 :
41 : static void
42 : my_stream_receive_cb( fd_quic_stream_t * stream,
43 : void * ctx,
44 : uchar const * data,
45 : ulong data_sz,
46 : ulong offset,
47 0 : int fin ) {
48 0 : (void)ctx; (void)stream; (void)data; (void)data_sz; (void)offset; (void)fin;
49 0 : }
50 :
51 : static FD_TL ulong g_clock;
52 :
53 : static ulong
54 4284 : test_clock( void * context FD_FN_UNUSED ) {
55 4284 : return g_clock;
56 4284 : }
57 :
58 : int
59 : LLVMFuzzerInitialize( int * pargc,
60 18 : char *** pargv ) {
61 18 : putenv( "FD_LOG_BACKTRACE=0" );
62 18 : fd_boot( pargc, pargv );
63 18 : atexit( fd_halt );
64 18 : fd_log_level_logfile_set(0);
65 18 : fd_log_level_stderr_set(0);
66 18 : return 0;
67 18 : }
68 :
69 : static int
70 : _aio_send( void * ctx,
71 : fd_aio_pkt_info_t const * batch,
72 : ulong batch_cnt,
73 : ulong * opt_batch_idx,
74 1071 : int flush ) {
75 1071 : (void)flush;
76 1071 : (void)batch;
77 1071 : (void)batch_cnt;
78 1071 : (void)opt_batch_idx;
79 1071 : (void)ctx;
80 1071 : return 0;
81 1071 : }
82 :
83 : static void
84 : send_udp_packet( fd_quic_t * quic,
85 : uchar const * data,
86 2088 : ulong size ) {
87 :
88 2088 : uchar buf[16384];
89 :
90 2088 : ulong headers_sz = sizeof(fd_eth_hdr_t) + sizeof(fd_ip4_hdr_t) + sizeof(fd_udp_hdr_t);
91 :
92 2088 : uchar * cur = buf;
93 2088 : uchar * end = buf + sizeof(buf);
94 :
95 2088 : fd_eth_hdr_t eth = { .net_type = FD_ETH_HDR_TYPE_IP };
96 2088 : fd_ip4_hdr_t ip4 = {
97 2088 : .verihl = FD_IP4_VERIHL(4,5),
98 2088 : .protocol = FD_IP4_HDR_PROTOCOL_UDP,
99 2088 : .net_tot_len = (ushort)( sizeof(fd_ip4_hdr_t)+sizeof(fd_udp_hdr_t)+size ),
100 2088 : };
101 2088 : fd_udp_hdr_t udp = {
102 2088 : .net_sport = 8000,
103 2088 : .net_dport = 8001,
104 2088 : .net_len = (ushort)( sizeof(fd_udp_hdr_t)+size ),
105 2088 : .check = 0
106 2088 : };
107 :
108 : /* Guaranteed to not overflow */
109 2088 : fd_quic_encode_eth( cur, (ulong)( end-cur ), ð ); cur += sizeof(fd_eth_hdr_t);
110 2088 : fd_quic_encode_ip4( cur, (ulong)( end-cur ), &ip4 ); cur += sizeof(fd_ip4_hdr_t);
111 2088 : fd_quic_encode_udp( cur, (ulong)( end-cur ), &udp ); cur += sizeof(fd_udp_hdr_t);
112 :
113 2088 : if( cur + size > end ) return;
114 2088 : fd_memcpy( cur, data, size );
115 :
116 : /* Main fuzz entrypoint */
117 :
118 2088 : fd_quic_process_packet( quic, buf, headers_sz + size );
119 2088 : }
120 :
121 : int
122 : LLVMFuzzerTestOneInput( uchar const * data,
123 2088 : ulong size ) {
124 :
125 2088 : fd_rng_t _rng[1]; fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, 0U, 0UL ) );
126 :
127 : /* Memory region to hold the QUIC instance */
128 2088 : static uchar quic_mem[ 1<<23 ] __attribute__((aligned(FD_QUIC_ALIGN)));
129 :
130 : /* Create ultra low limits for QUIC instance for maximum performance */
131 2088 : fd_quic_limits_t const quic_limits = {
132 2088 : .conn_cnt = 2,
133 2088 : .handshake_cnt = 2,
134 2088 : .conn_id_cnt = 4,
135 2088 : .rx_stream_cnt = 1,
136 2088 : .inflight_pkt_cnt = 8UL,
137 2088 : .stream_pool_cnt = 8UL
138 2088 : };
139 :
140 : /* Enable features depending on the last few bits. The last bits are
141 : pseudorandom (either ignored or belong to the MAC tag) */
142 2088 : uint last_byte = 0U;
143 2088 : if( size > 0 ) last_byte = data[ size-1 ];
144 2088 : int enable_retry = !!(last_byte & 1);
145 2088 : int role = (last_byte & 2) ? FD_QUIC_ROLE_SERVER : FD_QUIC_ROLE_CLIENT;
146 2088 : int established = !!(last_byte & 4);
147 :
148 2088 : assert( fd_quic_footprint( &quic_limits ) <= sizeof(quic_mem) );
149 2088 : void * shquic = fd_quic_new( quic_mem, &quic_limits );
150 2088 : fd_quic_t * quic = fd_quic_join( shquic );
151 :
152 2088 : fd_quic_config_anonymous( quic, role );
153 :
154 2088 : fd_tls_test_sign_ctx_t test_signer = fd_tls_test_sign_ctx( rng );
155 2088 : fd_quic_config_test_signer( quic, &test_signer );
156 :
157 2088 : quic->cb.stream_new = my_stream_new_cb;
158 2088 : quic->cb.stream_notify = my_stream_notify_cb;
159 2088 : quic->cb.stream_receive = my_stream_receive_cb;
160 2088 : quic->cb.now = test_clock;
161 2088 : quic->config.retry = enable_retry;
162 :
163 2088 : fd_aio_t aio_[1];
164 2088 : fd_aio_t * aio = fd_aio_join( fd_aio_new( aio_, NULL, _aio_send ) );
165 2088 : assert( aio );
166 :
167 2088 : fd_quic_set_aio_net_tx( quic, aio );
168 2088 : assert( fd_quic_init( quic ) );
169 2088 : assert( quic->config.idle_timeout > 0 );
170 :
171 2088 : fd_quic_state_t * state = fd_quic_get_state( quic );
172 :
173 : /* Create dummy connection */
174 2088 : ulong our_conn_id = 0UL;
175 2088 : fd_quic_conn_id_t peer_conn_id = { .sz=8 };
176 2088 : uint dst_ip_addr = 0U;
177 2088 : ushort dst_udp_port = (ushort)0;
178 :
179 2088 : fd_quic_conn_t * conn =
180 2088 : fd_quic_conn_create( quic,
181 2088 : our_conn_id, &peer_conn_id,
182 2088 : dst_ip_addr, (ushort)dst_udp_port,
183 2088 : 1 /* we are the server */ );
184 2088 : assert( conn );
185 2088 : assert( conn->svc_type == FD_QUIC_SVC_WAIT );
186 :
187 2088 : conn->tx_max_data = 512UL;
188 2088 : conn->tx_initial_max_stream_data_uni = 64UL;
189 2088 : conn->rx_max_data = 512UL;
190 2088 : conn->rx_sup_stream_id = 32UL;
191 2088 : conn->tx_max_datagram_sz = FD_QUIC_MTU;
192 2088 : conn->tx_sup_stream_id = 32UL;
193 :
194 2088 : if( established ) {
195 861 : conn->state = FD_QUIC_CONN_STATE_ACTIVE;
196 861 : conn->keys_avail = 0xff;
197 861 : }
198 :
199 2088 : g_clock = 1000UL;
200 :
201 : /* Calls fuzz entrypoint */
202 2088 : send_udp_packet( quic, data, size );
203 :
204 : /* svc_quota is the max number of service calls that we expect to
205 : schedule in response to a single packet. */
206 2088 : long svc_quota = fd_long_max( (long)size, 1000L );
207 :
208 3090 : while( state->svc_queue[ FD_QUIC_SVC_INSTANT ].tail!=UINT_MAX ) {
209 1002 : fd_quic_service( quic );
210 1002 : assert( --svc_quota > 0 );
211 1002 : }
212 2088 : assert( conn->svc_type != FD_QUIC_SVC_INSTANT );
213 :
214 : /* Generate ACKs */
215 2109 : while( state->svc_queue[ FD_QUIC_SVC_ACK_TX ].head != UINT_MAX ) {
216 21 : fd_quic_conn_t * conn = fd_quic_conn_at_idx( state, state->svc_queue[ FD_QUIC_SVC_ACK_TX ].head );
217 21 : g_clock = conn->svc_time;
218 21 : fd_quic_service( quic );
219 21 : assert( --svc_quota > 0 );
220 21 : }
221 2088 : assert( conn->svc_type != FD_QUIC_SVC_INSTANT &&
222 2088 : conn->svc_type != FD_QUIC_SVC_ACK_TX );
223 :
224 : /* Simulate conn timeout */
225 5349 : while( state->svc_queue[ FD_QUIC_SVC_WAIT ].head != UINT_MAX ) {
226 3261 : ulong idle_timeout_ts = conn->last_activity + quic->config.idle_timeout + 1UL;
227 3261 : fd_quic_conn_t * conn = fd_quic_conn_at_idx( state, state->svc_queue[ FD_QUIC_SVC_WAIT ].head );
228 :
229 : /* Idle timeouts should not be scheduled significantly late */
230 3261 : assert( conn->svc_time < idle_timeout_ts + (ulong)2e9 );
231 :
232 3261 : g_clock = conn->svc_time;
233 3261 : fd_quic_service( quic );
234 3261 : assert( --svc_quota > 0 );
235 3261 : }
236 2088 : assert( conn->svc_type == UINT_MAX );
237 2088 : assert( conn->state == FD_QUIC_CONN_STATE_DEAD || conn->state == FD_QUIC_CONN_STATE_INVALID );
238 :
239 2088 : fd_quic_delete( fd_quic_leave( fd_quic_fini( quic ) ) );
240 2088 : fd_aio_delete( fd_aio_leave( aio ) );
241 2088 : fd_rng_delete( fd_rng_leave( rng ) );
242 2088 : return 0;
243 2088 : }
244 :
245 : #if !FD_QUIC_DISABLE_CRYPTO
246 :
247 : static fd_quic_crypto_keys_t const keys[1] = {{
248 : .pkt_key = {0},
249 : .iv = {0},
250 : .hp_key = {0},
251 : }};
252 :
253 : /* guess_packet_size attempts to discover the end of a QUIC packet.
254 : Returns the total length (including GCM tag) on success, sets *pn_off
255 : to the packet number offset and *pn to the packet number. Returns
256 : 0UL on failure. */
257 :
258 : static ulong
259 : guess_packet_size( uchar const * data,
260 : ulong size,
261 0 : ulong * pn_off ) {
262 :
263 0 : uchar const * cur_ptr = data;
264 0 : ulong cur_sz = size;
265 :
266 0 : ulong pkt_num_pnoff = 0UL;
267 0 : ulong total_len = size;
268 :
269 0 : if( FD_UNLIKELY( size < 1 ) ) return FD_QUIC_PARSE_FAIL;
270 0 : uchar hdr_form = fd_quic_h0_hdr_form( *cur_ptr );
271 :
272 0 : ulong rc;
273 0 : if( hdr_form == 1 ) { /* long header */
274 :
275 0 : uchar long_packet_type = fd_quic_h0_long_packet_type( *cur_ptr );
276 0 : cur_ptr += 1; cur_sz -= 1UL;
277 0 : fd_quic_long_hdr_t long_hdr[1];
278 0 : rc = fd_quic_decode_long_hdr( long_hdr, cur_ptr, cur_sz );
279 0 : if( rc == FD_QUIC_PARSE_FAIL ) return 0UL;
280 0 : cur_ptr += rc; cur_sz -= rc;
281 :
282 0 : switch( long_packet_type ) {
283 0 : case FD_QUIC_PKT_TYPE_INITIAL: {
284 0 : fd_quic_initial_t initial[1];
285 0 : rc = fd_quic_decode_initial( initial, cur_ptr, cur_sz );
286 0 : if( rc == FD_QUIC_PARSE_FAIL ) return 0UL;
287 0 : cur_ptr += rc; cur_sz -= rc;
288 :
289 0 : pkt_num_pnoff = initial->pkt_num_pnoff;
290 0 : total_len = pkt_num_pnoff + initial->len;
291 0 : break;
292 0 : }
293 0 : case FD_QUIC_PKT_TYPE_HANDSHAKE: {
294 0 : fd_quic_handshake_t handshake[1];
295 0 : rc = fd_quic_decode_handshake( handshake, cur_ptr, cur_sz );
296 0 : if( rc == FD_QUIC_PARSE_FAIL ) return 0UL;
297 0 : cur_ptr += rc; cur_sz -= rc;
298 :
299 0 : pkt_num_pnoff = handshake->pkt_num_pnoff;
300 0 : total_len = pkt_num_pnoff + handshake->len;
301 0 : break;
302 0 : }
303 0 : case FD_QUIC_PKT_TYPE_RETRY:
304 : /* Do we need to decrypt Retry packets? I'm not sure */
305 : /* TODO correctly derive size of packet in case there is another
306 : packet following the retry packet */
307 0 : return 0UL;
308 0 : case FD_QUIC_PKT_TYPE_ZERO_RTT:
309 : /* No support for 0-RTT yet */
310 0 : return 0UL;
311 0 : default:
312 0 : __builtin_unreachable();
313 0 : }
314 :
315 0 : } else { /* short header */
316 :
317 0 : fd_quic_one_rtt_t one_rtt[1];
318 0 : one_rtt->dst_conn_id_len = 8;
319 0 : rc = fd_quic_decode_one_rtt( one_rtt, cur_ptr, cur_sz );
320 0 : if( rc == FD_QUIC_PARSE_FAIL ) return 0UL;
321 0 : cur_ptr += rc; cur_sz -= rc;
322 :
323 0 : pkt_num_pnoff = one_rtt->pkt_num_pnoff;
324 :
325 0 : }
326 :
327 0 : *pn_off = pkt_num_pnoff;
328 0 : return total_len;
329 0 : }
330 :
331 : /* decrypt_packet attempts to decrypt the first QUIC packet in the given
332 : buffer. data points to the first byte of the QUIC packet. size is
333 : the number of bytes until the end of the UDP datagram. Returns the
334 : number of bytes that belonged to the first packet (<= size) on
335 : success. Returns 0 on failure and leaves the packet (partially)
336 : encrypted. */
337 :
338 : static ulong
339 : decrypt_packet( uchar * const data,
340 0 : ulong const size ) {
341 :
342 0 : ulong pkt_num_pnoff = 0UL;
343 0 : ulong total_len = guess_packet_size( data, size, &pkt_num_pnoff );
344 0 : if( !total_len ) return 0UL;
345 :
346 : /* Decrypt the packet */
347 :
348 0 : int decrypt_res = fd_quic_crypto_decrypt_hdr( data, size, pkt_num_pnoff, keys );
349 0 : if( decrypt_res != FD_QUIC_SUCCESS ) return 0UL;
350 :
351 0 : uint pkt_number_sz = fd_quic_h0_pkt_num_len( data[0] ) + 1u;
352 0 : ulong pkt_number = fd_quic_pktnum_decode( data+pkt_num_pnoff, pkt_number_sz );
353 :
354 0 : decrypt_res =
355 0 : fd_quic_crypto_decrypt( data, size,
356 0 : pkt_num_pnoff, pkt_number,
357 0 : keys );
358 0 : if( decrypt_res != FD_QUIC_SUCCESS ) return 0UL;
359 :
360 0 : return fd_ulong_min( total_len + FD_QUIC_CRYPTO_TAG_SZ, size );
361 0 : }
362 :
363 : /* decrypt_payload attempts to remove packet protection of a UDP
364 : datagram payload in-place. Note that a UDP datagram can contain
365 : multiple QUIC packets. */
366 :
367 : static int
368 : decrypt_payload( uchar * data,
369 0 : ulong size ) {
370 :
371 0 : if( size < 16 ) return 0;
372 :
373 : /* Heuristic: If the last 16 bytes of the packet (the AES-GCM tag) are
374 : zero consider it an unencrypted packet */
375 :
376 0 : uint mask=0U;
377 0 : for( ulong j=0UL; j<16UL; j++ ) mask |= data[size-16+j];
378 0 : if( !mask ) return 1;
379 :
380 0 : uchar * cur_ptr = data;
381 0 : ulong cur_sz = size;
382 :
383 0 : do {
384 :
385 0 : ulong sz = decrypt_packet( cur_ptr, cur_sz );
386 0 : if( !sz ) return 0;
387 0 : assert( sz <= cur_sz ); /* prevent out of bounds */
388 :
389 0 : cur_ptr += sz; cur_sz -= sz;
390 :
391 0 : } while( cur_sz );
392 :
393 0 : return 1;
394 0 : }
395 :
396 : static ulong
397 : encrypt_packet( uchar * const data,
398 0 : ulong const size ) {
399 :
400 0 : uchar out[ FD_QUIC_MTU ];
401 :
402 0 : ulong pkt_num_pnoff = 0UL;
403 0 : ulong total_len = guess_packet_size( data, size, &pkt_num_pnoff );
404 0 : if( ( total_len < FD_QUIC_CRYPTO_TAG_SZ ) |
405 0 : ( total_len > size ) |
406 0 : ( total_len > sizeof(out) ) )
407 0 : return size;
408 :
409 0 : uchar first = data[0];
410 0 : ulong pkt_number_sz = ( first & 0x03u ) + 1;
411 :
412 0 : ulong out_sz = total_len;
413 0 : uchar const * hdr = data;
414 0 : ulong hdr_sz = pkt_num_pnoff + pkt_number_sz;
415 :
416 0 : ulong pkt_number = 0UL;
417 0 : for( ulong j = 0UL; j < pkt_number_sz; ++j ) {
418 0 : pkt_number = ( pkt_number << 8UL ) + (ulong)( hdr[pkt_num_pnoff + j] );
419 0 : }
420 :
421 0 : if( ( out_sz < hdr_sz ) |
422 0 : ( out_sz - hdr_sz < FD_QUIC_CRYPTO_TAG_SZ ) )
423 0 : return size;
424 :
425 0 : uchar const * pay = hdr + hdr_sz;
426 0 : ulong pay_sz = out_sz - hdr_sz - FD_QUIC_CRYPTO_TAG_SZ;
427 :
428 0 : int encrypt_res =
429 0 : fd_quic_crypto_encrypt( out, &out_sz,
430 0 : hdr, hdr_sz,
431 0 : pay, pay_sz,
432 0 : keys, keys,
433 0 : pkt_number );
434 0 : if( encrypt_res != FD_QUIC_SUCCESS )
435 0 : return size;
436 0 : assert( out_sz == total_len );
437 :
438 0 : fd_memcpy( data, out, out_sz );
439 0 : return out_sz;
440 0 : }
441 :
442 : static void
443 : encrypt_payload( uchar * data,
444 0 : ulong size ) {
445 :
446 0 : uchar * cur_ptr = data;
447 0 : ulong cur_sz = size;
448 :
449 0 : while( cur_sz ) {
450 0 : ulong sz = encrypt_packet( cur_ptr, cur_sz );
451 0 : assert( sz ); /* prevent infinite loop */
452 0 : assert( sz <= cur_sz ); /* prevent out of bounds */
453 :
454 0 : cur_ptr += sz; cur_sz -= sz;
455 0 : }
456 0 : }
457 :
458 : /* LLVMFuzzerCustomMutator has the following behavior:
459 :
460 : - If the input is not encrypted, mutates the raw input, and produces
461 : an encrypted output
462 : - If the input is encrypted, mutates the decrypted input, and
463 : produces another encrypted output
464 : - If the input appears to be encrypted but fails to decrypt, mutates
465 : the raw encrypted input, and produces another output that will fail
466 : to decrypt. */
467 :
468 : ulong
469 : LLVMFuzzerCustomMutator( uchar * data,
470 : ulong data_sz,
471 : ulong max_sz,
472 0 : uint seed ) {
473 0 : int ok = decrypt_payload( data, data_sz );
474 0 : data_sz = LLVMFuzzerMutate( data, data_sz, max_sz );
475 0 : if( ok ) encrypt_payload( data, data_sz );
476 0 : (void)seed;
477 0 : return data_sz;
478 0 : }
479 :
480 : /* Find a strategy for custom crossover of decrypted packets */
481 :
482 : #endif /* !FD_QUIC_DISABLE_CRYPTO */
|