test: cargo-fuzz targets on parser and framing
Four libFuzzer targets: - parse_command: Command::from_bytes never panics; bounded payload. - parse_radio_config: RadioConfig::from_bytes never panics. - cobs_decoder_feed: CobsDecoder::feed is panic-free across arbitrary byte streams split into arbitrary chunks. - cobs_roundtrip: structure-aware (via arbitrary crate) Response → cobs_encode_response → ucobs::decode yields identical raw bytes. All four smoke-tested at 50k runs each with no findings. Intended CI cadence is a weekly cron with a larger time budget; per-PR would be expensive. Requires nightly + cargo-fuzz.
This commit is contained in:
6 files changed
+241
No files matched your search
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "donglora-protocol-fuzz"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
heapless = "0.9"
|
||||
ucobs = "0.3"
|
||||
|
||||
[dependencies.donglora-protocol]
|
||||
path = ".."
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
|
||||
# Prevent this from being picked up by a workspace scan of the parent repo.
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "parse_command"
|
||||
path = "fuzz_targets/parse_command.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "parse_radio_config"
|
||||
path = "fuzz_targets/parse_radio_config.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "cobs_decoder_feed"
|
||||
path = "fuzz_targets/cobs_decoder_feed.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "cobs_roundtrip"
|
||||
path = "fuzz_targets/cobs_roundtrip.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,36 @@
|
||||
//! `CobsDecoder::feed` must never panic or yield an oversized payload
|
||||
//! when fed arbitrary bytes in arbitrary chunks.
|
||||
//!
|
||||
//! The input is split into chunks driven by a leading length-prefix
|
||||
//! stream so the fuzzer can exercise boundary handling.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use donglora_protocol::MAX_PAYLOAD;
|
||||
use donglora_protocol::framing::CobsDecoder;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut decoder = CobsDecoder::new();
|
||||
let mut cursor = 0;
|
||||
|
||||
// Use the first byte (if any) as a pseudo-chunk-schedule: low nibble
|
||||
// picks a split size 1..=16, high nibble is skipped. Subsequent
|
||||
// leading bytes drive subsequent splits.
|
||||
let mut sched_idx = 0;
|
||||
while cursor < data.len() {
|
||||
let split = if sched_idx < data.len() {
|
||||
((data[sched_idx] & 0x0F) as usize) + 1
|
||||
} else {
|
||||
16
|
||||
};
|
||||
sched_idx = sched_idx.saturating_add(1);
|
||||
let end = (cursor + split).min(data.len());
|
||||
decoder.feed(&data[cursor..end], |cmd| {
|
||||
if let donglora_protocol::Command::Transmit { payload, .. } = cmd {
|
||||
assert!(payload.len() <= MAX_PAYLOAD);
|
||||
}
|
||||
});
|
||||
cursor = end;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Structure-aware COBS roundtrip: build an arbitrary `Response`, encode
|
||||
//! it through `cobs_encode_response`, decode the framed bytes via
|
||||
//! `ucobs::decode`, and confirm the decoded payload matches the raw
|
||||
//! `write_to` output.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use donglora_protocol::framing::{MAX_FRAME, cobs_encode_response};
|
||||
use donglora_protocol::{Bandwidth, ErrorCode, RadioConfig, Response};
|
||||
use heapless::Vec as HVec;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
enum ArbResponse {
|
||||
Pong,
|
||||
Config(ArbConfig),
|
||||
RxPacket {
|
||||
rssi: i16,
|
||||
snr: i16,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
TxDone,
|
||||
Ok,
|
||||
Error(ArbError),
|
||||
MacAddress([u8; 6]),
|
||||
}
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
struct ArbConfig {
|
||||
freq_hz: u32,
|
||||
bw: u8,
|
||||
sf: u8,
|
||||
cr: u8,
|
||||
sync_word: u16,
|
||||
tx_power_dbm: i8,
|
||||
preamble_len: u16,
|
||||
cad: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Arbitrary, Clone, Copy)]
|
||||
enum ArbError {
|
||||
InvalidConfig,
|
||||
RadioBusy,
|
||||
TxTimeout,
|
||||
NotConfigured,
|
||||
NoDisplay,
|
||||
}
|
||||
|
||||
fn into_error(a: ArbError) -> ErrorCode {
|
||||
match a {
|
||||
ArbError::InvalidConfig => ErrorCode::InvalidConfig,
|
||||
ArbError::RadioBusy => ErrorCode::RadioBusy,
|
||||
ArbError::TxTimeout => ErrorCode::TxTimeout,
|
||||
ArbError::NotConfigured => ErrorCode::NotConfigured,
|
||||
ArbError::NoDisplay => ErrorCode::NoDisplay,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_config(c: &ArbConfig) -> RadioConfig {
|
||||
let bw = match c.bw % 10 {
|
||||
0 => Bandwidth::Khz7,
|
||||
1 => Bandwidth::Khz10,
|
||||
2 => Bandwidth::Khz15,
|
||||
3 => Bandwidth::Khz20,
|
||||
4 => Bandwidth::Khz31,
|
||||
5 => Bandwidth::Khz41,
|
||||
6 => Bandwidth::Khz62,
|
||||
7 => Bandwidth::Khz125,
|
||||
8 => Bandwidth::Khz250,
|
||||
_ => Bandwidth::Khz500,
|
||||
};
|
||||
RadioConfig {
|
||||
freq_hz: c.freq_hz,
|
||||
bw,
|
||||
sf: c.sf,
|
||||
cr: c.cr,
|
||||
sync_word: c.sync_word,
|
||||
tx_power_dbm: c.tx_power_dbm,
|
||||
preamble_len: c.preamble_len,
|
||||
cad: c.cad,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_response(a: ArbResponse) -> Response {
|
||||
match a {
|
||||
ArbResponse::Pong => Response::Pong,
|
||||
ArbResponse::Config(c) => Response::Config(into_config(&c)),
|
||||
ArbResponse::RxPacket { rssi, snr, payload } => {
|
||||
let mut p = HVec::<u8, { donglora_protocol::MAX_PAYLOAD }>::new();
|
||||
let take = payload.len().min(donglora_protocol::MAX_PAYLOAD);
|
||||
let _ = p.extend_from_slice(&payload[..take]);
|
||||
Response::RxPacket {
|
||||
rssi,
|
||||
snr,
|
||||
payload: p,
|
||||
}
|
||||
}
|
||||
ArbResponse::TxDone => Response::TxDone,
|
||||
ArbResponse::Ok => Response::Ok,
|
||||
ArbResponse::Error(e) => Response::Error(into_error(e)),
|
||||
ArbResponse::MacAddress(m) => Response::MacAddress(m),
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
let Ok(arb) = ArbResponse::arbitrary(&mut u) else {
|
||||
return;
|
||||
};
|
||||
let resp = into_response(arb);
|
||||
|
||||
let mut raw = [0u8; MAX_FRAME];
|
||||
let raw_n = resp.clone().write_to(&mut raw);
|
||||
|
||||
let mut write = [0u8; MAX_FRAME];
|
||||
let mut encode = [0u8; MAX_FRAME];
|
||||
let Some(framed) = cobs_encode_response(resp, &mut write, &mut encode) else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(*framed.last().unwrap(), 0x00);
|
||||
let mut decoded = [0u8; MAX_FRAME];
|
||||
let n = ucobs::decode(&framed[..framed.len() - 1], &mut decoded).unwrap();
|
||||
assert_eq!(&decoded[..n], &raw[..raw_n]);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
//! `Command::from_bytes` must never panic on arbitrary input.
|
||||
//!
|
||||
//! Any accepted command must also respect the heapless payload bound.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use donglora_protocol::{Command, MAX_PAYLOAD};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Some(Command::Transmit { payload, .. }) = Command::from_bytes(data) {
|
||||
assert!(payload.len() <= MAX_PAYLOAD);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
//! `RadioConfig::from_bytes` must never panic on arbitrary input.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use donglora_protocol::RadioConfig;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = RadioConfig::from_bytes(data);
|
||||
});
|
||||
Reference in new issue
Block a user