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:

TargetFunction under testWhy fuzz it
wab_confirmedweir_server::wab::format::parse_confirmedParses the on-disk .confirmed sidecar. Reached at startup by the drain's directory scan and at runtime when the drain writes confirmation records.
envelope_parseweir_core::Envelope::decodeDecodes the wire-format envelope from every push the daemon accepts. Every connected client gets to feed this function arbitrary bytes.
wab_segment_headerweir_server::wab::format::parse_segment_headerParses 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 by weir_server::wab::format::build_confirmed. Seed inputs can be generated by running the existing format tests in crates/weir-server/src/wab/format.rs::tests and dumping each generated buffer to fuzz/corpus/wab_confirmed/.
  • envelope_parse: the reference frames in crates/weir-core/tests/reference_frames.rs are already-bytes-on-disk and load straight into fuzz/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 in crates/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 exposing SegmentReader from the weir-server library facade (which re-introduces visibility-leak warnings because Metrics is pub(crate)) or extracting the parsing into a pure-bytes function in wab/format.rs. The pure-bytes extraction is the cleaner path and is a one-PR follow-up.
  • weir_server::config::Config TOML parsing — exposed surface is small (operator-controlled, runs once at startup), so the ROI of fuzzing it is low.