From 78c31d1bed7e931b51ec9ba8c832c83a8860a40b Mon Sep 17 00:00:00 2001 From: kingchenc Date: Fri, 22 May 2026 16:44:19 +0200 Subject: [PATCH] E16: add a cargo-fuzz harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repository had no fuzzing setup despite several natural targets — the CSV parser, the Binance envelope deserializer, and the stateful indicator/aggregator update paths. Add a fuzz/ cargo-fuzz crate (detached from the workspace via its own [workspace] table and the parent's exclude) with four targets: - csv_reader — CandleReader over arbitrary bytes - binance_envelope — RawWsEnvelope deserialization from arbitrary strings - indicator_update — RSI/EMA streaming + batch over arbitrary f64 series - tick_aggregator — TickAggregator over arbitrary tick triples Each target asserts the no-panic contract: malformed input must surface as an Err. fuzz/README.md documents running them (nightly + cargo-fuzz). --- Cargo.toml | 2 +- fuzz/.gitignore | 5 +++ fuzz/Cargo.toml | 46 +++++++++++++++++++++++++++ fuzz/README.md | 41 ++++++++++++++++++++++++ fuzz/fuzz_targets/binance_envelope.rs | 13 ++++++++ fuzz/fuzz_targets/csv_reader.rs | 17 ++++++++++ fuzz/fuzz_targets/indicator_update.rs | 22 +++++++++++++ fuzz/fuzz_targets/tick_aggregator.rs | 25 +++++++++++++++ 8 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/README.md create mode 100644 fuzz/fuzz_targets/binance_envelope.rs create mode 100644 fuzz/fuzz_targets/csv_reader.rs create mode 100644 fuzz/fuzz_targets/indicator_update.rs create mode 100644 fuzz/fuzz_targets/tick_aggregator.rs diff --git a/Cargo.toml b/Cargo.toml index dcd439f9..695e4971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "bindings/wasm", "bindings/node", ] -exclude = [] +exclude = ["fuzz"] [workspace.package] version = "0.1.4" diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..ab0eaa1a --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,5 @@ +target/ +corpus/ +artifacts/ +coverage/ +Cargo.lock diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..dab3fbba --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "wickra-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +serde_json = "1" +wickra-core = { path = "../crates/wickra-core" } +wickra-data = { path = "../crates/wickra-data", features = ["live-binance"] } + +# Detach this crate from the parent workspace — cargo-fuzz builds it on its +# own with sanitizer flags. +[workspace] + +[[bin]] +name = "csv_reader" +path = "fuzz_targets/csv_reader.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "binance_envelope" +path = "fuzz_targets/binance_envelope.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "indicator_update" +path = "fuzz_targets/indicator_update.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "tick_aggregator" +path = "fuzz_targets/tick_aggregator.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..5ac4d323 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,41 @@ +# Fuzzing Wickra + +[`cargo-fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz.html) harnesses for +the parsing and stateful entry points of Wickra. Fuzzing requires a nightly +Rust toolchain. + +## Setup + +```bash +cargo install cargo-fuzz +rustup toolchain install nightly +``` + +## Targets + +| Target | What it exercises | +| --- | --- | +| `csv_reader` | `CandleReader` over arbitrary bytes — headers, cells, BOM, binary noise. | +| `binance_envelope` | `RawWsEnvelope` deserialization from arbitrary strings. | +| `indicator_update` | RSI / EMA streaming + batch over arbitrary `f64` sequences (NaN, ±inf, jumps). | +| `tick_aggregator` | `TickAggregator` over arbitrary `(price, volume, timestamp)` triples. | + +## Run + +```bash +# From the repository root: +cargo +nightly fuzz run csv_reader +cargo +nightly fuzz run binance_envelope +cargo +nightly fuzz run indicator_update +cargo +nightly fuzz run tick_aggregator +``` + +Each run continues until a crash is found or it is interrupted. A short +time-boxed smoke run is useful in CI: + +```bash +cargo +nightly fuzz run csv_reader -- -max_total_time=60 +``` + +The expectation for every target is that it never panics: malformed or +adversarial input must surface as an `Err`, never a crash. diff --git a/fuzz/fuzz_targets/binance_envelope.rs b/fuzz/fuzz_targets/binance_envelope.rs new file mode 100644 index 00000000..6ce6d56d --- /dev/null +++ b/fuzz/fuzz_targets/binance_envelope.rs @@ -0,0 +1,13 @@ +#![no_main] +//! Fuzz the Binance combined-stream envelope deserializer. +//! +//! `RawWsEnvelope` is what a kline frame is decoded into; feeding it +//! arbitrary strings exercises the serde path that runs on every WebSocket +//! frame. It must reject malformed input with an `Err`, never panic. + +use libfuzzer_sys::fuzz_target; +use wickra_data::live::binance::RawWsEnvelope; + +fuzz_target!(|data: &str| { + let _ = serde_json::from_str::(data); +}); diff --git a/fuzz/fuzz_targets/csv_reader.rs b/fuzz/fuzz_targets/csv_reader.rs new file mode 100644 index 00000000..95ad95e7 --- /dev/null +++ b/fuzz/fuzz_targets/csv_reader.rs @@ -0,0 +1,17 @@ +#![no_main] +//! Fuzz the OHLCV CSV reader with arbitrary byte input. +//! +//! The reader must never panic: malformed headers, non-numeric cells, +//! truncated rows and arbitrary binary data all have to surface as an +//! `Err`, never a crash. + +use libfuzzer_sys::fuzz_target; +use wickra_data::csv::CandleReader; + +fuzz_target!(|data: &[u8]| { + if let Ok(mut reader) = CandleReader::from_reader(data) { + for candle in reader.candles() { + let _ = candle; + } + } +}); diff --git a/fuzz/fuzz_targets/indicator_update.rs b/fuzz/fuzz_targets/indicator_update.rs new file mode 100644 index 00000000..e3b640c0 --- /dev/null +++ b/fuzz/fuzz_targets/indicator_update.rs @@ -0,0 +1,22 @@ +#![no_main] +//! Fuzz indicator updates with arbitrary `f64` sequences. +//! +//! Every indicator must tolerate any finite-or-not input stream — NaN, ±inf, +//! subnormals, abrupt jumps — without panicking, and `batch` must agree with +//! the streaming `update` path. + +use libfuzzer_sys::fuzz_target; +use wickra_core::{BatchExt, Ema, Indicator, Rsi}; + +fuzz_target!(|data: Vec| { + let mut rsi = Rsi::new(14).unwrap(); + let mut ema = Ema::new(20).unwrap(); + for &x in &data { + let _ = rsi.update(x); + let _ = ema.update(x); + } + + // batch over the same data must not panic either. + let _ = Rsi::new(14).unwrap().batch(&data); + let _ = Ema::new(20).unwrap().batch(&data); +}); diff --git a/fuzz/fuzz_targets/tick_aggregator.rs b/fuzz/fuzz_targets/tick_aggregator.rs new file mode 100644 index 00000000..72f3ebc5 --- /dev/null +++ b/fuzz/fuzz_targets/tick_aggregator.rs @@ -0,0 +1,25 @@ +#![no_main] +//! Fuzz the tick-to-candle aggregator with arbitrary `(price, volume, +//! timestamp)` triples. +//! +//! The aggregator must never panic — out-of-order ticks and volume overflow +//! have to surface as an `Err`, and `Timeframe::floor` must not overflow for +//! any `i64` timestamp. + +use libfuzzer_sys::fuzz_target; +use wickra_core::Tick; +use wickra_data::aggregator::{TickAggregator, Timeframe}; + +fuzz_target!(|data: Vec<(f64, f64, i64)>| { + let mut agg = TickAggregator::new(Timeframe::new(60).unwrap()).with_gap_fill(true); + for (price, volume, ts) in data { + let Ok(tick) = Tick::new(price, volume, ts) else { + continue; + }; + if agg.push(tick).is_err() { + // An out-of-order tick is a defined error; stop feeding this run. + break; + } + } + let _ = agg.flush(); +});