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.
127 lines
3.4 KiB
Rust
127 lines
3.4 KiB
Rust
//! 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]);
|
|
});
|