test(server): exercise keyed stateful fuzz paths
This commit is contained in:
2 files changed
+128
-13
No files matched your search
+1
-1
@@ -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_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_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` |
|
| `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**.
|
The invariant every target asserts is: **arbitrary bytes must never panic**.
|
||||||
Returning `Err` is normal and expected; a successful decode is also normal.
|
Returning `Err` is normal and expected; a successful decode is also normal.
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use arbitrary::Unstructured;
|
use arbitrary::Unstructured;
|
||||||
|
use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
|
||||||
use irtt_server::{ServerConfig, ServerCore};
|
use irtt_server::{ServerConfig, ServerCore};
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
@@ -15,6 +19,13 @@ const MAX_DATAGRAM_LEN: usize = 2 * 1024;
|
|||||||
const MAX_SESSIONS: u8 = 8;
|
const MAX_SESSIONS: u8 = 8;
|
||||||
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-server-core-key";
|
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-server-core-key";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FuzzSession {
|
||||||
|
token: u64,
|
||||||
|
peer: SocketAddr,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
fuzz_target!(|data: &[u8]| {
|
fuzz_target!(|data: &[u8]| {
|
||||||
if data.len() > MAX_INPUT_LEN {
|
if data.len() > MAX_INPUT_LEN {
|
||||||
return;
|
return;
|
||||||
@@ -24,26 +35,34 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
let Ok(mut config) = config(&mut input) else {
|
let Ok(mut config) = config(&mut input) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if input.arbitrary::<bool>().unwrap_or(false) {
|
let hmac_key = input
|
||||||
config = config.with_hmac_key(FUZZ_HMAC_KEY);
|
.arbitrary::<bool>()
|
||||||
|
.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 core = ServerCore::new(config);
|
||||||
|
let mut sessions = Vec::new();
|
||||||
|
|
||||||
for _ in 0..MAX_DATAGRAMS {
|
for _ in 0..MAX_DATAGRAMS {
|
||||||
let Ok(peer) = peer(&mut input) else {
|
let Ok(operation) = input.arbitrary::<u8>() else {
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Ok(length) = input.int_in_range(0..=MAX_DATAGRAM_LEN) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Ok(packet) = input.bytes(length) else {
|
|
||||||
break;
|
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
|
// Arbitrary network input is expected to be rejected or to produce an
|
||||||
// internal error in exceptional host conditions. It must never panic,
|
// internal error in exceptional host conditions. It must never panic,
|
||||||
// and it may never grow the bounded table past its configured cap.
|
// 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());
|
assert!(core.session_count() <= core.config().max_sessions());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -52,7 +71,103 @@ fn config(input: &mut Unstructured<'_>) -> arbitrary::Result<ServerConfig> {
|
|||||||
let max_sessions = usize::from(input.int_in_range(0..=MAX_SESSIONS)?);
|
let max_sessions = usize::from(input.int_in_range(0..=MAX_SESSIONS)?);
|
||||||
Ok(ServerConfig::default()
|
Ok(ServerConfig::default()
|
||||||
.with_max_sessions(max_sessions)
|
.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<FuzzSession>,
|
||||||
|
) {
|
||||||
|
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::<u8>().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<FuzzSession>,
|
||||||
|
) {
|
||||||
|
let index = usize::from(input.arbitrary::<u8>().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<SocketAddr> {
|
fn peer(input: &mut Unstructured<'_>) -> arbitrary::Result<SocketAddr> {
|
||||||
|
|||||||
Reference in new issue
Block a user