feat(fuzz): add corpus seed generator with seeds for all targets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-26 00:08:24 +00:00
parent b0adff2a9b
commit 58f1e8b471
22 changed files with 309 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

309
lib/fuzz/gen_corpus.rs Normal file
View file

@ -0,0 +1,309 @@
use std::fs;
// USB/IP protocol constants
const USBIP_VERSION: u16 = 0x0111;
const OP_REQ_DEVLIST: u16 = 0x8005;
const OP_REQ_IMPORT: u16 = 0x8003;
const USBIP_CMD_SUBMIT: u16 = 0x0001;
const USBIP_CMD_UNLINK: u16 = 0x0002;
// Direction
const DIR_OUT: u32 = 0;
const DIR_IN: u32 = 1;
/// Write a seed file to corpus/<target>/seed-<name>
fn write_seed(target: &str, name: &str, data: &[u8]) {
let dir = format!("corpus/{target}");
fs::create_dir_all(&dir).unwrap();
let path = format!("{dir}/seed-{name}");
fs::write(&path, data).unwrap();
println!(" {path} ({} bytes)", data.len());
}
/// OP_REQ_DEVLIST: version(2) + command(2) + status(4) = 8 bytes
fn op_req_devlist() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&USBIP_VERSION.to_be_bytes());
buf.extend_from_slice(&OP_REQ_DEVLIST.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf
}
/// OP_REQ_IMPORT: version(2) + command(2) + status(4) + busid(32) = 40 bytes
fn op_req_import(busid: &str) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&USBIP_VERSION.to_be_bytes());
buf.extend_from_slice(&OP_REQ_IMPORT.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let mut busid_bytes = [0u8; 32];
let len = busid.len().min(32);
busid_bytes[..len].copy_from_slice(&busid.as_bytes()[..len]);
buf.extend_from_slice(&busid_bytes);
buf
}
/// USBIP_CMD_SUBMIT: 48-byte header + data (OUT only) + iso descriptors
fn cmd_submit(
seqnum: u32,
direction: u32,
ep: u32,
transfer_buffer_length: u32,
number_of_packets: u32,
interval: u32,
setup: [u8; 8],
data: &[u8],
iso_descriptors: &[u8],
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); // version (0 for URB phase)
buf.extend_from_slice(&USBIP_CMD_SUBMIT.to_be_bytes()); // command
buf.extend_from_slice(&seqnum.to_be_bytes()); // seqnum
buf.extend_from_slice(&2u32.to_be_bytes()); // devid
buf.extend_from_slice(&direction.to_be_bytes()); // direction
buf.extend_from_slice(&ep.to_be_bytes()); // ep
buf.extend_from_slice(&0u32.to_be_bytes()); // transfer_flags
buf.extend_from_slice(&transfer_buffer_length.to_be_bytes()); // transfer_buffer_length
buf.extend_from_slice(&0u32.to_be_bytes()); // start_frame
buf.extend_from_slice(&number_of_packets.to_be_bytes()); // number_of_packets
buf.extend_from_slice(&interval.to_be_bytes()); // interval
buf.extend_from_slice(&setup); // setup (8 bytes)
buf.extend_from_slice(data);
buf.extend_from_slice(iso_descriptors);
buf
}
/// USBIP_CMD_UNLINK: 48 bytes total
fn cmd_unlink(seqnum: u32, unlink_seqnum: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&USBIP_CMD_UNLINK.to_be_bytes());
buf.extend_from_slice(&seqnum.to_be_bytes());
buf.extend_from_slice(&2u32.to_be_bytes()); // devid
buf.extend_from_slice(&0u32.to_be_bytes()); // direction
buf.extend_from_slice(&0u32.to_be_bytes()); // ep
buf.extend_from_slice(&unlink_seqnum.to_be_bytes());
buf.extend_from_slice(&[0u8; 24]); // padding
buf
}
/// ISO packet descriptor: offset(4) + length(4) + actual_length(4) + status(4) = 16 bytes
fn iso_desc(offset: u32, length: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&offset.to_be_bytes());
buf.extend_from_slice(&length.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes()); // actual_length (client sets to 0)
buf.extend_from_slice(&0u32.to_be_bytes()); // status (client sets to 0)
buf
}
/// Control IN setup: GET_DESCRIPTOR
fn setup_get_desc(desc_type: u8, desc_index: u8, length: u16) -> [u8; 8] {
[
0x80, 0x06,
desc_index, desc_type,
0x00, 0x00,
(length & 0xFF) as u8, (length >> 8) as u8,
]
}
/// Control IN setup: GET_DESCRIPTOR with interface recipient
fn setup_get_desc_iface(desc_type: u8, desc_index: u8, iface: u16, length: u16) -> [u8; 8] {
[
0x81, 0x06,
desc_index, desc_type,
(iface & 0xFF) as u8, (iface >> 8) as u8,
(length & 0xFF) as u8, (length >> 8) as u8,
]
}
/// Control OUT setup: SET_CONFIGURATION
fn setup_set_config(config: u16) -> [u8; 8] {
[
0x00, 0x09,
(config & 0xFF) as u8, (config >> 8) as u8,
0x00, 0x00,
0x00, 0x00,
]
}
/// Control OUT setup: SET_INTERFACE (alternate setting)
fn setup_set_interface(iface: u16, alt: u16) -> [u8; 8] {
[
0x01, 0x0B,
(alt & 0xFF) as u8, (alt >> 8) as u8,
(iface & 0xFF) as u8, (iface >> 8) as u8,
0x00, 0x00,
]
}
/// Build a standard USB enumeration sequence (5 CMD_SUBMITs, seqnums 1-5)
fn enumeration_sequence() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend(cmd_submit(1, DIR_IN, 0, 64, 0, 0, setup_get_desc(1, 0, 64), &[], &[]));
buf.extend(cmd_submit(2, DIR_IN, 0, 18, 0, 0, setup_get_desc(1, 0, 18), &[], &[]));
buf.extend(cmd_submit(3, DIR_IN, 0, 9, 0, 0, setup_get_desc(2, 0, 9), &[], &[]));
buf.extend(cmd_submit(4, DIR_IN, 0, 255, 0, 0, setup_get_desc(2, 0, 255), &[], &[]));
buf.extend(cmd_submit(5, DIR_OUT, 0, 0, 0, 0, setup_set_config(1), &[], &[]));
buf
}
fn gen_parse_command() {
println!("fuzz_parse_command:");
write_seed("fuzz_parse_command", "devlist", &op_req_devlist());
write_seed("fuzz_parse_command", "import", &op_req_import("0-0-0"));
}
fn gen_urb_hid() {
println!("fuzz_urb_hid:");
// Seed 1: Standard enumeration sequence
write_seed("fuzz_urb_hid", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + HID-specific requests
let mut buf = enumeration_sequence();
let seq = 6;
// GET_DESCRIPTOR(HID Report Descriptor, type=0x22, 128 bytes) on interface 0
buf.extend(cmd_submit(seq, DIR_IN, 0, 128, 0, 0,
setup_get_desc_iface(0x22, 0, 0, 128), &[], &[]));
// SET_IDLE (class request: bmRequestType=0x21, bRequest=0x0A)
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
// GET_REPORT (class request: bmRequestType=0xA1, bRequest=0x01, report type=Input(1))
buf.extend(cmd_submit(seq + 2, DIR_IN, 0, 8, 0, 0,
[0xA1, 0x01, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00], &[], &[]));
write_seed("fuzz_urb_hid", "hid-class-requests", &buf);
// Seed 3: Interrupt IN polls on ep 0x81 (keyboard reports)
let mut buf = enumeration_sequence();
for i in 0..4u32 {
buf.extend(cmd_submit(6 + i, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
}
write_seed("fuzz_urb_hid", "hid-interrupt-in", &buf);
// Seed 4: CMD_SUBMIT followed by CMD_UNLINK
let mut buf = cmd_submit(1, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]);
buf.extend(cmd_unlink(2, 1));
write_seed("fuzz_urb_hid", "unlink", &buf);
// Seed 5: Zero-length control transfer (GET_STATUS)
write_seed("fuzz_urb_hid", "get-status",
&cmd_submit(1, DIR_IN, 0, 2, 0, 0,
[0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], &[], &[]));
}
fn gen_urb_cdc() {
println!("fuzz_urb_cdc:");
// Seed 1: Standard enumeration
write_seed("fuzz_urb_cdc", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + CDC class requests
let mut buf = enumeration_sequence();
let seq = 6;
// SET_LINE_CODING: bmRequestType=0x21, bRequest=0x20
// Data: 115200 baud (LE), 1 stop bit, no parity, 8 data bits
let line_coding: [u8; 7] = [0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08];
buf.extend(cmd_submit(seq, DIR_OUT, 0, 7, 0, 0,
[0x21, 0x20, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00], &line_coding, &[]));
// SET_CONTROL_LINE_STATE: DTR + RTS active (wValue=0x0003)
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x22, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-class-requests", &buf);
// Seed 3: Bulk OUT data transfer on ep 0x02
let mut buf = enumeration_sequence();
let test_data = b"Hello, CDC!\r\n";
buf.extend(cmd_submit(6, DIR_OUT, 0x02, test_data.len() as u32, 0, 0,
[0u8; 8], test_data, &[]));
write_seed("fuzz_urb_cdc", "cdc-bulk-out", &buf);
// Seed 4: Bulk IN on ep 0x82 (ep=2, direction=IN)
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_IN, 0x02, 512, 0, 0, [0u8; 8], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-bulk-in", &buf);
// Seed 5: Interrupt IN on ep 0x81 (notification, ep=1, direction=IN)
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-interrupt-in", &buf);
}
fn gen_urb_uac() {
println!("fuzz_urb_uac:");
// Seed 1: Standard enumeration
write_seed("fuzz_urb_uac", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + SET_INTERFACE to activate audio streaming
let mut buf = enumeration_sequence();
let seq = 6;
buf.extend(cmd_submit(seq, DIR_OUT, 0, 0, 0, 0, setup_set_interface(1, 1), &[], &[]));
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0, setup_set_interface(2, 1), &[], &[]));
write_seed("fuzz_urb_uac", "uac-set-interface", &buf);
// Seed 3: Isochronous OUT on ep 0x01 (playback, 1 frame of 48kHz 16-bit stereo = 192 bytes)
let audio_frame = vec![0x80u8; 192];
let iso = iso_desc(0, 192);
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(1, 1), &[], &[]));
buf.extend(cmd_submit(7, DIR_OUT, 0x01, 192, 1, 1, [0u8; 8], &audio_frame, &iso));
write_seed("fuzz_urb_uac", "uac-iso-out", &buf);
// Seed 4: Isochronous IN on ep 0x82 (capture)
let iso = iso_desc(0, 192);
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(2, 1), &[], &[]));
buf.extend(cmd_submit(7, DIR_IN, 0x02, 192, 1, 1, [0u8; 8], &[], &iso));
write_seed("fuzz_urb_uac", "uac-iso-in", &buf);
// Seed 5: Multi-packet ISO OUT (3 frames)
let audio_data = vec![0x80u8; 576];
let mut iso = Vec::new();
iso.extend(iso_desc(0, 192));
iso.extend(iso_desc(192, 192));
iso.extend(iso_desc(384, 192));
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(1, 1), &[], &[]));
buf.extend(cmd_submit(7, DIR_OUT, 0x01, 576, 3, 1, [0u8; 8], &audio_data, &iso));
write_seed("fuzz_urb_uac", "uac-iso-out-multi", &buf);
}
fn gen_handle_client() {
println!("fuzz_handle_client:");
// Seed 1: OP_REQ_DEVLIST only (tests early negotiation path)
write_seed("fuzz_handle_client", "devlist-only", &op_req_devlist());
// Seed 2: OP_REQ_DEVLIST then OP_REQ_IMPORT (two-step negotiation)
let mut buf = op_req_devlist();
buf.extend(op_req_import("0-0-0"));
write_seed("fuzz_handle_client", "devlist-then-import", &buf);
// Seed 3: OP_REQ_IMPORT + enumeration sequence
let mut buf = op_req_import("0-0-0");
buf.extend(enumeration_sequence());
write_seed("fuzz_handle_client", "import-enumerate", &buf);
// Seed 4: Full session — import + enumerate + HID class requests + interrupt
let mut buf = op_req_import("0-0-0");
buf.extend(enumeration_sequence());
let seq = 6;
buf.extend(cmd_submit(seq, DIR_IN, 0, 128, 0, 0,
setup_get_desc_iface(0x22, 0, 0, 128), &[], &[]));
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
for i in 0..4u32 {
buf.extend(cmd_submit(seq + 2 + i, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
}
write_seed("fuzz_handle_client", "import-hid-full", &buf);
}
fn main() {
println!("Generating fuzz corpus seeds...\n");
gen_parse_command();
gen_urb_hid();
gen_urb_cdc();
gen_urb_uac();
gen_handle_client();
println!("\nDone.");
}