feat: add fuzz_helpers module with response validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72e9ba292e
commit
c59bb719d5
1 changed files with 224 additions and 0 deletions
224
lib/src/fuzz_helpers.rs
Normal file
224
lib/src/fuzz_helpers.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
//! Response validation for fuzz targets.
|
||||
//!
|
||||
//! Validates that USB/IP responses written to the output side of a MockSocket
|
||||
//! are well-formed, catching issues that could confuse the host kernel's vhci_hcd.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::usbip_protocol::{
|
||||
OP_REP_DEVLIST, OP_REP_IMPORT, USBIP_RET_SUBMIT, USBIP_RET_UNLINK, USBIP_VERSION,
|
||||
};
|
||||
|
||||
/// Linux errno: ECONNRESET = 104
|
||||
const ECONNRESET: i32 = 104;
|
||||
|
||||
/// Validate all complete USB/IP responses in the output buffer.
|
||||
///
|
||||
/// Silently returns if the buffer is empty or ends with a partial response
|
||||
/// (expected when the fuzzer hits EOF mid-stream).
|
||||
///
|
||||
/// Panics (triggering a fuzz finding) if any complete response violates
|
||||
/// structural or semantic invariants.
|
||||
pub fn assert_usbip_responses_valid(output: &[u8]) {
|
||||
let mut offset = 0;
|
||||
let mut seen_seqnums: HashSet<u32> = HashSet::new();
|
||||
|
||||
while offset < output.len() {
|
||||
// Try to parse negotiation-phase responses first (version + command header)
|
||||
if output.len() - offset >= 8 {
|
||||
let version = u16::from_be_bytes([output[offset], output[offset + 1]]);
|
||||
let command = u16::from_be_bytes([output[offset + 2], output[offset + 3]]);
|
||||
|
||||
if version == USBIP_VERSION {
|
||||
match command {
|
||||
OP_REP_DEVLIST => {
|
||||
offset = validate_op_rep_devlist(output, offset);
|
||||
if offset == 0 {
|
||||
return; // partial response, ok
|
||||
}
|
||||
continue;
|
||||
}
|
||||
OP_REP_IMPORT => {
|
||||
offset = validate_op_rep_import(output, offset);
|
||||
if offset == 0 {
|
||||
return; // partial response, ok
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
panic!(
|
||||
"Unknown negotiation response command: {command:#06x} at offset {offset}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URB-phase responses: 48-byte header minimum
|
||||
if output.len() - offset < 48 {
|
||||
return; // partial response, ok
|
||||
}
|
||||
|
||||
let command = u32::from_be_bytes(output[offset..offset + 4].try_into().unwrap());
|
||||
let seqnum = u32::from_be_bytes(output[offset + 4..offset + 8].try_into().unwrap());
|
||||
let _devid = u32::from_be_bytes(output[offset + 8..offset + 12].try_into().unwrap());
|
||||
let direction = u32::from_be_bytes(output[offset + 12..offset + 16].try_into().unwrap());
|
||||
let _ep = u32::from_be_bytes(output[offset + 16..offset + 20].try_into().unwrap());
|
||||
|
||||
assert!(
|
||||
direction <= 1,
|
||||
"Invalid direction {direction} in response at offset {offset}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
seen_seqnums.insert(seqnum),
|
||||
"Duplicate seqnum {seqnum} in response at offset {offset}"
|
||||
);
|
||||
|
||||
match command {
|
||||
cmd if cmd == u32::from(USBIP_RET_SUBMIT) => {
|
||||
let status =
|
||||
i32::from_be_bytes(output[offset + 20..offset + 24].try_into().unwrap());
|
||||
let actual_length =
|
||||
u32::from_be_bytes(output[offset + 24..offset + 28].try_into().unwrap());
|
||||
let _start_frame =
|
||||
u32::from_be_bytes(output[offset + 28..offset + 32].try_into().unwrap());
|
||||
let number_of_packets =
|
||||
i32::from_be_bytes(output[offset + 32..offset + 36].try_into().unwrap());
|
||||
let _error_count =
|
||||
u32::from_be_bytes(output[offset + 36..offset + 40].try_into().unwrap());
|
||||
// bytes 40..48 are padding
|
||||
|
||||
offset += 48;
|
||||
|
||||
// Transfer buffer follows the header
|
||||
let transfer_buf_len = if status < 0 {
|
||||
// On error, no transfer data
|
||||
0u32
|
||||
} else if direction == 1 {
|
||||
// IN: actual_length bytes of data
|
||||
actual_length
|
||||
} else {
|
||||
// OUT: no data in response
|
||||
0
|
||||
};
|
||||
|
||||
if output.len() - offset < transfer_buf_len as usize {
|
||||
return; // partial response, ok
|
||||
}
|
||||
offset += transfer_buf_len as usize;
|
||||
|
||||
// ISO packet descriptors follow the transfer buffer
|
||||
let iso_count = if number_of_packets > 0 && status >= 0 {
|
||||
number_of_packets as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let iso_bytes = iso_count as usize * 16;
|
||||
if output.len() - offset < iso_bytes {
|
||||
return; // partial response, ok
|
||||
}
|
||||
|
||||
// Validate each ISO packet descriptor
|
||||
for i in 0..iso_count {
|
||||
let iso_offset = offset + (i as usize * 16);
|
||||
let pkt_offset = u32::from_be_bytes(
|
||||
output[iso_offset..iso_offset + 4].try_into().unwrap(),
|
||||
);
|
||||
let _pkt_length = u32::from_be_bytes(
|
||||
output[iso_offset + 4..iso_offset + 8].try_into().unwrap(),
|
||||
);
|
||||
let pkt_actual_length = u32::from_be_bytes(
|
||||
output[iso_offset + 8..iso_offset + 12].try_into().unwrap(),
|
||||
);
|
||||
let _pkt_status = i32::from_be_bytes(
|
||||
output[iso_offset + 12..iso_offset + 16].try_into().unwrap(),
|
||||
);
|
||||
|
||||
// actual data referenced must be within transfer buffer
|
||||
if pkt_actual_length > 0 {
|
||||
assert!(
|
||||
pkt_offset.saturating_add(pkt_actual_length) <= actual_length,
|
||||
"ISO packet {i}: offset({pkt_offset}) + actual_length({pkt_actual_length}) \
|
||||
exceeds transfer buffer length({actual_length}) at response offset {offset}"
|
||||
);
|
||||
}
|
||||
}
|
||||
offset += iso_bytes;
|
||||
}
|
||||
cmd if cmd == u32::from(USBIP_RET_UNLINK) => {
|
||||
let status =
|
||||
i32::from_be_bytes(output[offset + 20..offset + 24].try_into().unwrap());
|
||||
// bytes 24..48 are padding
|
||||
|
||||
assert!(
|
||||
status == 0 || status == -(ECONNRESET as i32),
|
||||
"RET_UNLINK status must be 0 or -ECONNRESET(-{ECONNRESET}), got {status} \
|
||||
at offset {offset}"
|
||||
);
|
||||
|
||||
offset += 48;
|
||||
}
|
||||
_ => {
|
||||
panic!("Unknown URB response command: {command:#010x} at offset {offset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate an OP_REP_DEVLIST response starting at `start`.
|
||||
/// Returns the new offset after the response, or 0 if the response is partial.
|
||||
fn validate_op_rep_devlist(output: &[u8], start: usize) -> usize {
|
||||
// Header: version(2) + command(2) + status(4) + device_count(4) = 12
|
||||
if output.len() - start < 12 {
|
||||
return 0;
|
||||
}
|
||||
let status = u32::from_be_bytes(output[start + 4..start + 8].try_into().unwrap());
|
||||
assert_eq!(status, 0, "OP_REP_DEVLIST status must be 0, got {status}");
|
||||
|
||||
let device_count =
|
||||
u32::from_be_bytes(output[start + 8..start + 12].try_into().unwrap());
|
||||
|
||||
let mut offset = start + 12;
|
||||
|
||||
// Each device: 0x138 bytes (312) + 4 bytes per interface
|
||||
for _i in 0..device_count {
|
||||
if output.len() - offset < 0x138 {
|
||||
return 0; // partial
|
||||
}
|
||||
let num_interfaces = u32::from_be_bytes(
|
||||
output[offset + 0x134..offset + 0x138].try_into().unwrap(),
|
||||
);
|
||||
offset += 0x138;
|
||||
|
||||
let intf_bytes = num_interfaces as usize * 4;
|
||||
if output.len() - offset < intf_bytes {
|
||||
return 0; // partial
|
||||
}
|
||||
offset += intf_bytes;
|
||||
}
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
/// Validate an OP_REP_IMPORT response starting at `start`.
|
||||
/// Returns the new offset after the response, or 0 if the response is partial.
|
||||
fn validate_op_rep_import(output: &[u8], start: usize) -> usize {
|
||||
// Header: version(2) + command(2) + status(4) = 8
|
||||
if output.len() - start < 8 {
|
||||
return 0;
|
||||
}
|
||||
let status = u32::from_be_bytes(output[start + 4..start + 8].try_into().unwrap());
|
||||
|
||||
if status == 0 {
|
||||
// Success: device descriptor follows (0x138 bytes)
|
||||
if output.len() - start < 8 + 0x138 {
|
||||
return 0; // partial
|
||||
}
|
||||
start + 8 + 0x138
|
||||
} else {
|
||||
// Failure: no device descriptor
|
||||
start + 8
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue