Merge pull request #116 from Lochnair/test/proto-fuzzing
test(proto): add decoder fuzzing
This commit was merged in pull request #116.
This commit is contained in:
29 files changed
+774
No files matched your search
@@ -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
|
||||||
@@ -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