test(server): fuzz ServerCore datagram handling

This commit is contained in:
Nils Andreas Svee committed 2026-09-15 12:39:59 +07:00
1 parent 4563816873
commit bc2bf34ed4
5 files changed
+215 -8

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
name: Proto Fuzz Smoke name: Fuzz Smoke
on: on:
push: push:
@@ -71,7 +71,7 @@ jobs:
# regression; this is not meant to be exhaustive fuzzing. # regression; this is not meant to be exhaustive fuzzing.
run: | run: |
set -euo pipefail set -euo pipefail
for target in decode_request decode_open_reply decode_echo_reply decode_params decode_varint; do for target in decode_request decode_open_reply decode_echo_reply decode_params decode_varint server_core_datagrams; do
echo "::group::fuzz smoke: ${target}" echo "::group::fuzz smoke: ${target}"
RUSTUP_TOOLCHAIN="${FUZZ_TOOLCHAIN}" cargo fuzz run --target "${FUZZ_TARGET}" "${target}" -- \ RUSTUP_TOOLCHAIN="${FUZZ_TOOLCHAIN}" cargo fuzz run --target "${FUZZ_TARGET}" "${target}" -- \
-max_total_time=10 \ -max_total_time=10 \
+127
View File
@@ -8,6 +8,18 @@ version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.12.1" version = "0.12.1"
@@ -35,6 +47,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]] [[package]]
name = "cmov" name = "cmov"
version = "0.5.4" version = "0.5.4"
@@ -126,10 +144,24 @@ dependencies = [
name = "irtt-proto-fuzz" name = "irtt-proto-fuzz"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"arbitrary",
"irtt-proto", "irtt-proto",
"irtt-server",
"libfuzzer-sys", "libfuzzer-sys",
] ]
[[package]]
name = "irtt-server"
version = "0.5.2"
dependencies = [
"getrandom",
"irtt-proto",
"nix",
"socket2",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.35" version = "0.1.35"
@@ -166,6 +198,45 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "mio"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -196,6 +267,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -233,6 +314,31 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.20.1" version = "1.20.1"
@@ -244,3 +350,24 @@ name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+11
View File
@@ -8,11 +8,15 @@ edition = "2021"
cargo-fuzz = true cargo-fuzz = true
[dependencies] [dependencies]
arbitrary = "1.4.2"
libfuzzer-sys = "0.4.13" libfuzzer-sys = "0.4.13"
[dependencies.irtt-proto] [dependencies.irtt-proto]
path = "../crates/irtt-proto" path = "../crates/irtt-proto"
[dependencies.irtt-server]
path = "../crates/irtt-server"
# Use independent workspace for fuzzers # Use independent workspace for fuzzers
[workspace] [workspace]
members = ["."] members = ["."]
@@ -51,3 +55,10 @@ path = "fuzz_targets/decode_varint.rs"
test = false test = false
doc = false doc = false
bench = false bench = false
[[bin]]
name = "server_core_datagrams"
path = "fuzz_targets/server_core_datagrams.rs"
test = false
doc = false
bench = false
+9 -6
View File
@@ -1,6 +1,7 @@
# `irtt-proto` fuzzing # `irtt-rs` fuzzing
Coverage-guided fuzzing of `irtt-proto`'s wire decoders via Coverage-guided fuzzing of `irtt-proto` wire decoders and `irtt-server`'s public
`ServerCore::handle_datagram` API via
[`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)/libFuzzer. [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)/libFuzzer.
This project is intentionally isolated from the normal stable/MSRV This project is intentionally isolated from the normal stable/MSRV
@@ -22,14 +23,16 @@ 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 |
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.
Every target bounds its input to 128 KiB (`MAX_INPUT_LEN` in each harness) so Every target bounds its input (`MAX_INPUT_LEN` in each harness) so libFuzzer
libFuzzer spends its time exploring protocol structure rather than spends its time exploring protocol structure rather than allocating/copying
allocating/copying megabytes — production IRTT datagrams are UDP-sized, well megabytes. `server_core_datagrams` additionally caps its sequence length and
under this bound. each individual datagram and configures the core to the same packet cap, so one
libFuzzer input cannot become a tiny DoS benchmark.
## Running ## Running
@@ -0,0 +1,66 @@
#![no_main]
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use arbitrary::Unstructured;
use irtt_server::{ServerConfig, ServerCore};
use libfuzzer_sys::fuzz_target;
/// Bounds one libFuzzer input as well as the work it can ask the core to do.
/// These are deliberately much smaller than a production UDP payload: the
/// target is stateful parser/session exploration, not packet-size throughput.
const MAX_INPUT_LEN: usize = 32 * 1024;
const MAX_DATAGRAMS: usize = 16;
const MAX_DATAGRAM_LEN: usize = 2 * 1024;
const MAX_SESSIONS: u8 = 8;
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-server-core-key";
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
let mut input = Unstructured::new(data);
let Ok(mut config) = config(&mut input) else {
return;
};
if input.arbitrary::<bool>().unwrap_or(false) {
config = config.with_hmac_key(FUZZ_HMAC_KEY);
}
let mut core = ServerCore::new(config);
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 {
break;
};
// 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());
}
});
fn config(input: &mut Unstructured<'_>) -> arbitrary::Result<ServerConfig> {
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))
}
fn peer(input: &mut Unstructured<'_>) -> arbitrary::Result<SocketAddr> {
let port = input.arbitrary()?;
let address = if input.arbitrary()? {
IpAddr::V4(Ipv4Addr::from(input.arbitrary::<[u8; 4]>()?))
} else {
IpAddr::V6(Ipv6Addr::from(input.arbitrary::<[u8; 16]>()?))
};
Ok(SocketAddr::new(address, port))
}