Fuzzing
weir's on-disk and on-wire byte parsers sit on trust boundaries: the WAB confirmed-file format is read at daemon startup (post-restart the bytes are attacker-controlled if the host was compromised), and the wire envelope decoder reads every byte off an unauthenticated socket connection. A panic or unbounded allocation in either parser turns into a denial-of-service vector.
This directory ships a cargo-fuzz setup with three targets:
| Target | Function under test | Why fuzz it |
|---|---|---|
wab_confirmed | weir_server::wab::format::parse_confirmed | Parses the on-disk .confirmed sidecar. Reached at startup by the drain's directory scan and at runtime when the drain writes confirmation records. |
envelope_parse | weir_core::Envelope::decode | Decodes the wire-format envelope from every push the daemon accepts. Every connected client gets to feed this function arbitrary bytes. |
wab_segment_header | weir_server::wab::format::parse_segment_header | Parses the header of every .wab/.wab.sealed file — read at daemon startup (crash recovery's directory scan) and on every drain, so attacker-controlled bytes after a host compromise reach it directly. Exists because format v2 added version-routing and a flags-byte validation v1 never had. |
The existing proptest harness in crates/weir-core/tests/reference_frames.rs
covers round-tripping — encode → decode of valid envelopes always
yields the original. The fuzz targets cover the inverse: arbitrary
input never panics, regardless of whether it round-trips.
Running
cargo-fuzz requires a nightly Rust toolchain (libFuzzer's sanitizer
plumbing isn't on stable):
rustup install nightly
cargo install cargo-fuzz
Then, from the repo root:
# 60-second smoke run of one target:
cd fuzz
cargo +nightly fuzz run wab_confirmed -- -max_total_time=60
# Longer runs surface deeper coverage:
cargo +nightly fuzz run envelope_parse -- -max_total_time=600
# All targets, sequentially, 60 s each:
for t in $(cargo +nightly fuzz list); do
cargo +nightly fuzz run "$t" -- -max_total_time=60
done
cargo fuzz builds a release-with-sanitizer binary, then invokes
libFuzzer's coverage-guided loop. On a panic or sanitizer hit it
writes the offending input to fuzz/artifacts/<target>/. Reproduce
locally with cargo +nightly fuzz run <target> fuzz/artifacts/<target>/<crash-file>.
Seed corpus
For coverage-guided fuzzing, libFuzzer benefits from a seed corpus of "interesting" inputs to start from. Two natural sources for these parsers:
weir_confirmed: the bytes written byweir_server::wab::format::build_confirmed. Seed inputs can be generated by running the existing format tests incrates/weir-server/src/wab/format.rs::testsand dumping each generated buffer tofuzz/corpus/wab_confirmed/.envelope_parse: the reference frames incrates/weir-core/tests/reference_frames.rsare already-bytes-on-disk and load straight intofuzz/corpus/envelope_parse/.
The corpus directories are gitignored — each invocation of
cargo fuzz run re-discovers and grows them.
CI integration
Compiled, not run. The harnesses job in .github/workflows/ci.yml
builds every fuzz target on nightly Rust (cd fuzz && cargo +nightly build --all-targets) on every push, alongside a cargo check --all-targets of the
chaos harness. That catches a target that no longer compiles — the state
this doc used to describe, when nothing built these targets at all — but it
does not run libFuzzer for a single iteration. A target that builds clean and
panics on the first input fed to it still passes this job.
Actually fuzzing — even the 60-second smoke run above — still needs
nightly's sanitizer plumbing and a real time budget, and is not wired into
CI. It stays a manual step: cd fuzz && cargo +nightly fuzz run <target> -- -max_total_time=60. A scheduled or PR-triggered fuzz run is still a real
follow-up; the compile check does not substitute for it.
What's deliberately NOT fuzzed (yet)
SegmentReader::open + iterate— the per-record loop incrates/weir-server/src/wab/mod.rs. Reaches MAX_PAYLOAD_HARD_CAP before allocating, so the worst case is "drop an error"; we'd still like coverage. Adding the target requires either exposingSegmentReaderfrom theweir-serverlibrary facade (which re-introduces visibility-leak warnings becauseMetricsispub(crate)) or extracting the parsing into a pure-bytes function inwab/format.rs. The pure-bytes extraction is the cleaner path and is a one-PR follow-up.weir_server::config::ConfigTOML parsing — exposed surface is small (operator-controlled, runs once at startup), so the ROI of fuzzing it is low.