test(proto): add decoder fuzzing #116

Merged
Lochnair merged 2 commits from test/proto-fuzzing into main 2026-08-19 17:31:41 +00:00
29 changed files with 774 additions and 0 deletions

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
name: Proto Fuzz Smoke
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Pinned for reproducibility; bump deliberately alongside `fuzz/Cargo.lock`.
CARGO_FUZZ_VERSION: 0.13.1
jobs:
fuzz-smoke:
name: cargo fuzz smoke
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
cache-key: fuzz
- name: Install nightly Rust toolchain
# Installed alongside, but deliberately not made the directory
# default: cargo-fuzz itself is built with the default stable
# toolchain below, and only `fuzz build`/`fuzz run` are run with
# `+nightly`. Building cargo-fuzz's own dependency tree under a
# nightly override can trip build scripts (e.g. rustix) that gate
# unstable internal attributes on detecting a nightly compiler.
run: rustup toolchain install nightly --profile minimal --no-self-update
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked --version "${CARGO_FUZZ_VERSION}"
- name: Build all fuzz targets
run: cargo +nightly fuzz build
working-directory: fuzz
- name: Run bounded fuzz smoke for every target
# A single job building once and running each target sequentially
# avoids recompiling the fuzz workspace per target. Ten seconds per
# target is enough to catch a broken harness or an obvious
# regression; this is not meant to be exhaustive fuzzing.
run: |
set -euo pipefail
for target in decode_request decode_open_reply decode_echo_reply decode_params decode_varint; do
echo "::group::fuzz smoke: ${target}"
cargo +nightly fuzz run "${target}" -- \
-max_total_time=10 \
-timeout=5
echo "::endgroup::"
done
working-directory: fuzz
+10
View File
@@ -0,0 +1,10 @@
target
artifacts
coverage
# The committed seed corpus under corpus/ is small and curated by hand; a
# local `cargo fuzz run` grows each target's corpus directory far beyond
# that, so ignore anything not already tracked instead of the directory
# itself.
corpus/*/*
!corpus/*/
+246
View File
@@ -0,0 +1,246 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "cc"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cmov"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"ctutils",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hmac"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest",
]
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "irtt-proto"
version = "0.5.0"
dependencies = [
"hmac",
"md-5",
"subtle",
"thiserror",
]
[[package]]
name = "irtt-proto-fuzz"
version = "0.0.0"
dependencies = [
"irtt-proto",
"libfuzzer-sys",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "md-5"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+53
View File
@@ -0,0 +1,53 @@
[package]
name = "irtt-proto-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.irtt-proto]
path = "../crates/irtt-proto"
# Use independent workspace for fuzzers
[workspace]
members = ["."]
[[bin]]
name = "decode_request"
path = "fuzz_targets/decode_request.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decode_open_reply"
path = "fuzz_targets/decode_open_reply.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decode_echo_reply"
path = "fuzz_targets/decode_echo_reply.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decode_params"
path = "fuzz_targets/decode_params.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decode_varint"
path = "fuzz_targets/decode_varint.rs"
test = false
doc = false
bench = false
+77
View File
@@ -0,0 +1,77 @@
# `irtt-proto` fuzzing
Coverage-guided fuzzing of `irtt-proto`'s wire decoders via
[`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)/libFuzzer.
This project is intentionally isolated from the normal stable/MSRV
workspace: it lives in its own `[workspace]` under `fuzz/`, is never a member
of the root workspace, requires nightly Rust, and is not built by
`cargo check`/`test`/`clippy`/`doc` at the workspace root.
## Prerequisites
- nightly Rust (`rustup toolchain install nightly`)
- `cargo-fuzz` (`cargo install cargo-fuzz --locked --version 0.13.1`)
## Targets
| Target | Exercises |
| ------------------- | ----------------------------------------------------------------- |
| `decode_request` | `decode_request`, `Params::decode` on a decoded Open, `verify_packet_hmac` |
| `decode_open_reply` | `decode_open_reply`, 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_varint` | `varint::decode_uvarint`/`decode_varint` and their round trip through `encode_uvarint`/`encode_varint` |
The invariant every target asserts is: **arbitrary bytes must never panic**.
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
libFuzzer spends its time exploring protocol structure rather than
allocating/copying megabytes — production IRTT datagrams are UDP-sized, well
under this bound.
## Running
```sh
cargo +nightly fuzz run decode_request
```
Bounded smoke run, e.g. 30 seconds:
```sh
cargo +nightly fuzz run decode_request -- -max_total_time=30
```
Build every target without running:
```sh
cargo +nightly fuzz build
```
## Corpus
Seed corpora live under `fuzz/corpus/<target>/`. They were generated once
from `irtt-proto`'s own public encoders (`encode_request`, `encode_open_reply`,
`encode_echo_reply`, `Params::encode`) by a throwaway local script, not copied
from any external source, and not derived from upstream `heistp/irtt`. Each
file is a small, representative wire packet (minimal Open, Close, Echo, HMAC
variants, ordinary/recv-count/recv-window/midpoint Echo replies, and a few
`Params` payloads including an unknown tag and `server_fill`).
## Crash artifacts
A failing case is written under `fuzz/artifacts/<target>/`. To replay it:
```sh
cargo +nightly fuzz run decode_request fuzz/artifacts/decode_request/<crash-file>
```
## Isolation notes
- `fuzz/Cargo.toml` declares its own `[workspace]`, so it is never picked up
by the root `[workspace]` in `../Cargo.toml`.
- `libfuzzer-sys` and `arbitrary` are fuzz-only dependencies; they never
appear in any published crate's dependency graph.
- `fuzz/Cargo.lock` is committed for reproducible fuzzing builds, independent
of the root `Cargo.lock`.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
§[ u9ü;dº%Íý¶Ä¢•oRq‡«¶–x€ø‚­€¨Ö¹
Binary file not shown.
@@ -0,0 +1 @@
ァ[Rq�カ堀€�ュ€ィヨケ
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
€�ュ€ィヨケ
@@ -0,0 +1 @@

+1
View File
@@ -0,0 +1 @@
 pattern:abc
+1
View File
@@ -0,0 +1 @@
c�
+1
View File
@@ -0,0 +1 @@
ァ[�fUD3"
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
§[ Ай ;¦w6Џос†{‘в‚Ђш‚­ЂЁЦ№
+1
View File
@@ -0,0 +1 @@
ァ[
+1
View File
@@ -0,0 +1 @@
ァ[€�ュ€ィヨケ
+133
View File
@@ -0,0 +1,133 @@
#![no_main]
use irtt_proto::{
decode_echo_reply, echo_packet_len, hmac::compute_hmac_in_place, Clock, PacketLayout, Params,
ReceivedStats, StampAt, FLAG_HMAC, FLAG_REPLY, TIMESTAMP_SIZE,
};
use libfuzzer_sys::fuzz_target;
/// See `decode_request.rs` for the rationale behind this bound.
const MAX_INPUT_LEN: usize = 128 * 1024;
/// Mirrors `irtt-proto`'s crate-private HMAC field offset, the same way the
/// crate's own external integration tests do.
const HMAC_OFFSET: usize = 4;
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-decode-echo-reply-key";
/// Negotiated-length vocabulary around the boundaries that matter for this
/// decoder: zero, tiny, MTU-ish, and modestly larger. Deliberately bounded so
/// we fuzz the decoder rather than benchmark OOM handling.
const LENGTH_BUCKETS: &[i64] = &[
0, 1, 63, 64, 65, 127, 128, 129, 511, 512, 513, 1199, 1200, 1201, 1471, 1472, 1473, 1499, 1500,
1501, 4096, 65507,
];
fn pick_received_stats(v: u8) -> ReceivedStats {
match v % 4 {
0 => ReceivedStats::None,
1 => ReceivedStats::Count,
2 => ReceivedStats::Window,
_ => ReceivedStats::Both,
}
}
fn pick_stamp_at(v: u8) -> StampAt {
match v % 5 {
0 => StampAt::None,
1 => StampAt::Send,
2 => StampAt::Receive,
3 => StampAt::Both,
_ => StampAt::Midpoint,
}
}
fn pick_clock(v: u8) -> Clock {
match v % 4 {
0 => Clock::Unspecified,
1 => Clock::Wall,
2 => Clock::Monotonic,
_ => Clock::Both,
}
}
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
// Need enough control bytes to derive Params before any packet bytes.
const CONTROL_LEN: usize = 6;
if data.len() < CONTROL_LEN {
return;
}
let (control, rest) = data.split_at(CONTROL_LEN);
let received_stats = pick_received_stats(control[0]);
let stamp_at = pick_stamp_at(control[1]);
let clock = pick_clock(control[2]);
let length_bucket = LENGTH_BUCKETS[control[3] as usize % LENGTH_BUCKETS.len()];
let hmac_present = control[4] & 1 == 1;
let use_compat_len = control[5] & 1 == 1;
let params = Params {
protocol_version: 1,
length: length_bucket,
received_stats,
stamp_at,
clock,
..Params::default()
};
let Ok(normal_len) = echo_packet_len(hmac_present, &params) else {
return;
};
let layout = PacketLayout::echo(hmac_present, &params);
let is_single_clock_midpoint =
stamp_at == StampAt::Midpoint && matches!(clock, Clock::Wall | Clock::Monotonic);
let target_len = if use_compat_len && is_single_clock_midpoint {
(layout.header_len() + TIMESTAMP_SIZE).max(normal_len)
} else {
normal_len
};
let flags = FLAG_REPLY | if hmac_present { FLAG_HMAC } else { 0 };
let mut packet = Vec::with_capacity(target_len);
packet.extend_from_slice(&irtt_proto::MAGIC);
packet.push(flags);
if hmac_present {
packet.extend_from_slice(&[0u8; 16]);
}
// Fill the remainder of the negotiated length from the fuzz-provided
// tail, truncating or zero-padding to hit the target length exactly so
// the decoder's field parsing gets real coverage rather than bailing out
// on a length mismatch every time.
let body_len = target_len.saturating_sub(packet.len());
if rest.len() >= body_len {
packet.extend_from_slice(&rest[..body_len]);
} else {
packet.extend_from_slice(rest);
packet.resize(target_len, 0);
}
packet.resize(target_len, 0);
if hmac_present {
if compute_hmac_in_place(FUZZ_HMAC_KEY, &mut packet, HMAC_OFFSET).is_err() {
return;
}
let reply = decode_echo_reply(&packet, &params, Some(FUZZ_HMAC_KEY));
if let Ok(reply) = reply {
assert!(reply.payload.len() <= packet.len());
}
} else {
let reply = decode_echo_reply(&packet, &params, None);
if let Ok(reply) = reply {
assert!(reply.payload.len() <= packet.len());
}
}
// Also exercise the raw fuzz bytes directly against the same negotiated
// Params, unmodified and un-lengthened, purely for the no-panic
// invariant on genuinely arbitrary bytes.
let _ = decode_echo_reply(data, &params, None);
});
+49
View File
@@ -0,0 +1,49 @@
#![no_main]
use irtt_proto::{
decode_open_reply, hmac::compute_hmac_in_place, FLAG_HMAC, FLAG_OPEN, FLAG_REPLY,
};
use libfuzzer_sys::fuzz_target;
/// See `decode_request.rs` for the rationale behind this bound.
const MAX_INPUT_LEN: usize = 128 * 1024;
/// Offset of the HMAC field, right after the 4-byte header. `irtt-proto`
/// keeps the real constant crate-private; this mirrors it the same way the
/// crate's own external integration tests do.
const HMAC_OFFSET: usize = 4;
const HMAC_SIZE: usize = 16;
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-decode-open-reply-key";
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
// Unauthenticated decode path: no panic is the invariant, Ok/Err both
// acceptable.
let _ = decode_open_reply(data, None);
// Authenticated decode path against arbitrary bytes. Most inputs will
// fail on HMAC presence/verification before reaching the post-HMAC
// parser, which is expected and fine on its own.
let _ = decode_open_reply(data, Some(FUZZ_HMAC_KEY));
// To also give the post-HMAC parser sustained coverage, derive a packet
// that *will* authenticate: when the input is structurally capable of
// carrying an HMAC field and its flags already indicate HMAC, patch in a
// freshly-computed HMAC over the untouched body via the crate's own
// helper (never reimplemented here) and decode that.
if data.len() >= HMAC_OFFSET + HMAC_SIZE {
let flags = data[3];
let looks_like_open_reply =
flags & FLAG_OPEN != 0 && flags & FLAG_REPLY != 0 && flags & FLAG_HMAC != 0;
if looks_like_open_reply {
let mut signed = data.to_vec();
if compute_hmac_in_place(FUZZ_HMAC_KEY, &mut signed, HMAC_OFFSET).is_ok() {
let _ = decode_open_reply(&signed, Some(FUZZ_HMAC_KEY));
}
}
}
});
+46
View File
@@ -0,0 +1,46 @@
#![no_main]
use irtt_proto::Params;
use libfuzzer_sys::fuzz_target;
/// See `decode_request.rs` for the rationale behind this bound.
const MAX_INPUT_LEN: usize = 128 * 1024;
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
let decoded = Params::decode(data);
let decoded_with_presence = Params::decode_with_presence(data);
// Both entry points share one parser, so when both succeed they must
// agree on the decoded values.
match (&decoded, &decoded_with_presence) {
(Ok(params), Ok(with_presence)) => {
assert_eq!(params, &with_presence.params);
}
(Ok(_), Err(_)) | (Err(_), Ok(_)) => {
panic!("decode and decode_with_presence disagreed on success for {data:02x?}");
}
(Err(_), Err(_)) => {}
}
// No panic is the primary invariant for malformed input; unknown tags are
// tolerated and repeated known tags are last-value-wins by design, so we
// do not assert a particular failure mode here.
if let Ok(params) = decoded {
let encoded = params.encode();
// Re-encoding and decoding again must preserve the semantic value.
// Presence information does not round-trip through `encode` for
// absent/default fields, so we compare `Params` values, not the
// original wire bytes.
if let Ok(reencoded_params) = Params::decode(&encoded) {
assert_eq!(params, reencoded_params);
} else {
panic!(
"re-encoding a successfully decoded Params produced bytes that failed to decode"
);
}
}
});
+45
View File
@@ -0,0 +1,45 @@
#![no_main]
use irtt_proto::{decode_request, verify_packet_hmac, DecodedRequestKind, Params};
use libfuzzer_sys::fuzz_target;
/// Keeps individual cases bounded so libFuzzer explores protocol structure
/// instead of spending time allocating/copying megabytes. This is UDP wire
/// protocol code; production datagrams are well under this bound.
const MAX_INPUT_LEN: usize = 128 * 1024;
/// Fixed key used only to exercise the HMAC verification path. Never treated
/// as a secret.
const FUZZ_HMAC_KEY: &[u8] = b"fuzz-decode-request-key";
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
// The core invariant: arbitrary bytes must never panic. Ok/Err are both
// normal outcomes.
let decoded = match decode_request(data) {
Ok(decoded) => decoded,
Err(_) => return,
};
match decoded.kind {
DecodedRequestKind::Open { params, .. } => {
// Structural Open decoding deliberately permits malformed
// parameter bytes; both Ok and Err from the secondary parser are
// acceptable, and a structurally decoded Open is not required to
// contain valid Params.
let _ = Params::decode(params);
}
DecodedRequestKind::Close { .. } | DecodedRequestKind::Echo { .. } => {
// No token/session policy belongs here; that's above irtt-proto.
}
}
if decoded.hmac_present {
// Most arbitrary HMAC fields will fail authentication; both outcomes
// are acceptable, we're only exercising the verifier for panics.
let _ = verify_packet_hmac(FUZZ_HMAC_KEY, data);
}
});
+38
View File
@@ -0,0 +1,38 @@
#![no_main]
use irtt_proto::varint::{decode_uvarint, decode_varint, encode_uvarint, encode_varint};
use libfuzzer_sys::fuzz_target;
/// See `decode_request.rs` for the rationale behind this bound. Varint
/// grammar is trivial, but the bound is kept for consistency with the other
/// targets.
const MAX_INPUT_LEN: usize = 128 * 1024;
fuzz_target!(|data: &[u8]| {
if data.len() > MAX_INPUT_LEN {
return;
}
if let Ok((value, used)) = decode_uvarint(data) {
assert!(used > 0);
assert!(used <= data.len());
assert!(used <= 10);
let mut encoded = Vec::new();
encode_uvarint(value, &mut encoded);
// Canonical re-encoding need not reproduce noncanonical fuzz input,
// but it must decode back to the same value and consume exactly its
// own bytes.
assert_eq!(decode_uvarint(&encoded), Ok((value, encoded.len())));
}
if let Ok((value, used)) = decode_varint(data) {
assert!(used > 0);
assert!(used <= data.len());
assert!(used <= 10);
let mut encoded = Vec::new();
encode_varint(value, &mut encoded);
assert_eq!(decode_varint(&encoded), Ok((value, encoded.len())));
}
});