Files
protocol-rs/fuzz/fuzz_targets/cobs_roundtrip.rs
T
swaits 0d0dc81879 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.
2026-04-13 09:05:07 -06:00

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]);
});