diff --git a/fuzz/README.md b/fuzz/README.md index dad8ed2..92b029b 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -23,7 +23,7 @@ of the root workspace, requires nightly Rust, and is not built by | `decode_echo_reply` | `decode_echo_reply` against a deterministic, bounded `Params` matrix, unauthenticated and HMAC-authenticated | | `decode_params` | `Params::decode` and `Params::decode_with_presence`, plus an encode/decode round trip | | `decode_varint` | `varint::decode_uvarint`/`decode_varint` and their round trip through `encode_uvarint`/`encode_varint` | -| `server_core_datagrams` | A bounded sequence of datagrams against one `ServerCore`, with varied IPv4/IPv6 peers and session/HMAC configuration; it asserts the session cap after every datagram | +| `server_core_datagrams` | A bounded mix of raw datagrams and production-encoded Open/Echo/Close requests against one `ServerCore`; it varies IPv4/IPv6 peers and session/HMAC configuration, authenticates keyed requests, reuses issued tokens, and asserts the session cap | The invariant every target asserts is: **arbitrary bytes must never panic**. Returning `Err` is normal and expected; a successful decode is also normal. diff --git a/fuzz/fuzz_targets/server_core_datagrams.rs b/fuzz/fuzz_targets/server_core_datagrams.rs index 5079af5..a746bc7 100644 --- a/fuzz/fuzz_targets/server_core_datagrams.rs +++ b/fuzz/fuzz_targets/server_core_datagrams.rs @@ -1,8 +1,12 @@ #![no_main] -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + time::Duration, +}; use arbitrary::Unstructured; +use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode}; use irtt_server::{ServerConfig, ServerCore}; use libfuzzer_sys::fuzz_target; @@ -15,6 +19,13 @@ const MAX_DATAGRAM_LEN: usize = 2 * 1024; const MAX_SESSIONS: u8 = 8; const FUZZ_HMAC_KEY: &[u8] = b"fuzz-server-core-key"; +#[derive(Clone)] +struct FuzzSession { + token: u64, + peer: SocketAddr, + params: Params, +} + fuzz_target!(|data: &[u8]| { if data.len() > MAX_INPUT_LEN { return; @@ -24,26 +35,34 @@ fuzz_target!(|data: &[u8]| { let Ok(mut config) = config(&mut input) else { return; }; - if input.arbitrary::().unwrap_or(false) { - config = config.with_hmac_key(FUZZ_HMAC_KEY); + let hmac_key = input + .arbitrary::() + .ok() + .filter(|enabled| *enabled) + .map(|_| FUZZ_HMAC_KEY); + if let Some(key) = hmac_key { + config = config.with_hmac_key(key); } let mut core = ServerCore::new(config); + let mut sessions = Vec::new(); for _ in 0..MAX_DATAGRAMS { - let Ok(peer) = peer(&mut input) else { - break; - }; - let Ok(length) = input.int_in_range(0..=MAX_DATAGRAM_LEN) else { - break; - }; - let Ok(packet) = input.bytes(length) else { + let Ok(operation) = input.arbitrary::() else { break; }; + match operation % 4 { + // Keep arbitrary-byte admission coverage alongside the stateful + // production-encoded requests below. + 0 => raw_datagram(&mut core, &mut input), + 1 => open_session(&mut core, &mut input, hmac_key, &mut sessions), + 2 => echo_session(&mut core, &mut input, hmac_key, &sessions), + _ => close_session(&mut core, &mut input, hmac_key, &mut sessions), + } + // Arbitrary network input is expected to be rejected or to produce an // internal error in exceptional host conditions. It must never panic, // and it may never grow the bounded table past its configured cap. - let _ = core.handle_datagram(peer, packet); assert!(core.session_count() <= core.config().max_sessions()); } }); @@ -52,7 +71,103 @@ fn config(input: &mut Unstructured<'_>) -> arbitrary::Result { let max_sessions = usize::from(input.int_in_range(0..=MAX_SESSIONS)?); Ok(ServerConfig::default() .with_max_sessions(max_sessions) - .with_max_packet_length(MAX_DATAGRAM_LEN)) + .with_max_packet_length(MAX_DATAGRAM_LEN) + .with_min_send_interval(Duration::ZERO)) +} + +fn raw_datagram(core: &mut ServerCore, input: &mut Unstructured<'_>) { + let (Ok(peer), Ok(length)) = (peer(input), input.int_in_range(0..=MAX_DATAGRAM_LEN)) else { + return; + }; + let Ok(packet) = input.bytes(length) else { + return; + }; + let _ = core.handle_datagram(peer, packet); +} + +fn open_session( + core: &mut ServerCore, + input: &mut Unstructured<'_>, + hmac_key: Option<&[u8]>, + sessions: &mut Vec, +) { + let Ok(peer) = peer(input) else { + return; + }; + let params = session_params(); + let request = encode_request( + RequestToEncode::Open { + params: ¶ms, + no_test: false, + }, + hmac_key, + ) + .expect("fixed valid fuzz open must encode"); + let Ok(Some(reply)) = core.handle_datagram(peer, &request) else { + return; + }; + let Ok(reply) = decode_open_reply(reply.bytes(), hmac_key) else { + return; + }; + sessions.push(FuzzSession { + token: reply.token, + peer, + params: reply.params, + }); +} + +fn echo_session( + core: &mut ServerCore, + input: &mut Unstructured<'_>, + hmac_key: Option<&[u8]>, + sessions: &[FuzzSession], +) { + let Some(session) = + sessions.get(usize::from(input.arbitrary::().unwrap_or(0)) % sessions.len().max(1)) + else { + return; + }; + let sequence = input.arbitrary().unwrap_or_default(); + let request = encode_request( + RequestToEncode::Echo { + token: session.token, + sequence, + params: &session.params, + payload: &[], + }, + hmac_key, + ) + .expect("fixed valid fuzz echo must encode"); + let _ = core.handle_datagram(session.peer, &request); +} + +fn close_session( + core: &mut ServerCore, + input: &mut Unstructured<'_>, + hmac_key: Option<&[u8]>, + sessions: &mut Vec, +) { + let index = usize::from(input.arbitrary::().unwrap_or(0)) % sessions.len().max(1); + let Some(session) = sessions.get(index) else { + return; + }; + let request = encode_request( + RequestToEncode::Close { + token: session.token, + }, + hmac_key, + ) + .expect("fixed valid fuzz close must encode"); + let _ = core.handle_datagram(session.peer, &request); + sessions.swap_remove(index); +} + +fn session_params() -> Params { + Params { + protocol_version: 1, + length: 256, + ..Params::default() + } } fn peer(input: &mut Unstructured<'_>) -> arbitrary::Result {