test(server): fuzz ServerCore datagram handling
This commit is contained in:
5 files changed
+215
-8
No files matched your search
@@ -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 \
|
||||||
|
|||||||
Generated
+127
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
@@ -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))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user