Files
protocol-rs/fuzz/fuzz_targets/command_roundtrip.rs
T
swaits ef576ba1ec test: three more cargo-fuzz targets + test-fuzz-long recipe
- command_roundtrip: structure-aware (via arbitrary crate) Command
  encode → parse → equal. Symmetric to cobs_roundtrip.
- cobs_decoder_multi_frame: chunked feeding with arbitrary split sizes;
  asserts bounded payload and that command count never exceeds sentinel
  count.
- cobs_chunk_invariant: same stream through two decoders (chunked vs
  all-at-once) must produce the same command sequence.

Adds test-fuzz-long recipe (600s per target, ~1h 10m total) for CI
cron / pre-release. All seven targets smoke-tested at 100k runs each
with zero findings.
2026-04-13 12:12:12 -06:00

158 lines
4.1 KiB
Rust

//! Structure-aware Command roundtrip: construct an arbitrary Command,
//! hand-encode it using the same wire layout the library parses, and
//! confirm `Command::from_bytes` recovers the original value exactly.
//!
//! The library itself is asymmetric (firmware only decodes commands),
//! so this fuzz target carries a matching encoder.
#![no_main]
use arbitrary::{Arbitrary, Unstructured};
use donglora_protocol::{Bandwidth, Command, MAX_PAYLOAD, RADIO_CONFIG_SIZE, RadioConfig};
use heapless::Vec as HVec;
use libfuzzer_sys::fuzz_target;
#[derive(Debug, Arbitrary)]
enum ArbCommand {
Ping,
GetConfig,
SetConfig(ArbConfig),
StartRx,
StopRx,
Transmit {
config: Option<ArbConfig>,
payload: Vec<u8>,
},
DisplayOn,
DisplayOff,
GetMac,
}
#[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,
}
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_command(a: &ArbCommand) -> Command {
match a {
ArbCommand::Ping => Command::Ping,
ArbCommand::GetConfig => Command::GetConfig,
ArbCommand::SetConfig(c) => Command::SetConfig(into_config(c)),
ArbCommand::StartRx => Command::StartRx,
ArbCommand::StopRx => Command::StopRx,
ArbCommand::Transmit { config, payload } => {
let cfg = config.as_ref().map(into_config);
let mut p: HVec<u8, MAX_PAYLOAD> = HVec::new();
let take = payload.len().min(MAX_PAYLOAD);
let _ = p.extend_from_slice(&payload[..take]);
Command::Transmit {
config: cfg,
payload: p,
}
}
ArbCommand::DisplayOn => Command::DisplayOn,
ArbCommand::DisplayOff => Command::DisplayOff,
ArbCommand::GetMac => Command::GetMac,
}
}
fn encode(cmd: &Command, out: &mut [u8]) -> usize {
match cmd {
Command::Ping => {
out[0] = 0;
1
}
Command::GetConfig => {
out[0] = 1;
1
}
Command::SetConfig(cfg) => {
out[0] = 2;
cfg.write_to(&mut out[1..]);
1 + RADIO_CONFIG_SIZE
}
Command::StartRx => {
out[0] = 3;
1
}
Command::StopRx => {
out[0] = 4;
1
}
Command::Transmit { config, payload } => {
out[0] = 5;
let mut pos = 1;
if let Some(cfg) = config {
out[pos] = 1;
pos += 1;
cfg.write_to(&mut out[pos..]);
pos += RADIO_CONFIG_SIZE;
} else {
out[pos] = 0;
pos += 1;
}
out[pos..pos + 2].copy_from_slice(&(payload.len() as u16).to_le_bytes());
pos += 2;
out[pos..pos + payload.len()].copy_from_slice(payload);
pos + payload.len()
}
Command::DisplayOn => {
out[0] = 6;
1
}
Command::DisplayOff => {
out[0] = 7;
1
}
Command::GetMac => {
out[0] = 8;
1
}
}
}
fuzz_target!(|data: &[u8]| {
let mut u = Unstructured::new(data);
let Ok(arb) = ArbCommand::arbitrary(&mut u) else {
return;
};
let cmd = into_command(&arb);
let mut buf = [0u8; 1024];
let n = encode(&cmd, &mut buf);
let parsed = Command::from_bytes(&buf[..n]).expect("encoded command must parse");
assert_eq!(parsed, cmd);
});