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(); +});