test(proto): add decoder fuzzing
Add isolated cargo-fuzz/libFuzzer infrastructure for irtt-proto, fuzzing decode_request, decode_open_reply, decode_echo_reply, Params::decode(_with_presence), and the varint codec against arbitrary and structurally-derived wire input. Each target's core invariant is that arbitrary bytes must never panic; a small clean-room seed corpus (generated from irtt-proto's own encoders) and a bounded nightly smoke CI workflow are included. The fuzz project is its own isolated cargo workspace and does not affect the root workspace, its lockfile, or any published crate's dependency graph.
This commit is contained in:
29 files changed
+766
No files matched your search
@@ -0,0 +1,59 @@
|
|||||||
|
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 nightly Rust toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
cache-key: fuzz
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -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/*/
|
||||||
Generated
+246
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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.
@@ -0,0 +1 @@
|
|||||||
|
§[u9ü;dº%Íý¶Ä¢•oRq‡«¶–x€ø‚€¨Ö¹
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
ァ[Rq�カ堀�ュィヨケ
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
�ュィヨケ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pattern:abc
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
c�
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ァ[�fUD3"
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
§[ Ай ;¦w6Џос†{‘в‚Ђш‚ЂЁЦ№
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ァ[
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ァ[�ュィヨケ
|
||||||
@@ -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, ¶ms) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = PacketLayout::echo(hmac_present, ¶ms);
|
||||||
|
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, ¶ms, Some(FUZZ_HMAC_KEY));
|
||||||
|
if let Ok(reply) = reply {
|
||||||
|
assert!(reply.payload.len() <= packet.len());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let reply = decode_echo_reply(&packet, ¶ms, 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, ¶ms, None);
|
||||||
|
});
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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())));
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user