fix: passthrough descriptors, SET_INTERFACE forwarding, and error mapping

- Forward Configuration, BOS, and DeviceQualifier descriptors to the
  real device in passthrough mode, preserving IADs, class-specific
  descriptors (audio, HID, DFU), and endpoint companions
- Forward SET_INTERFACE to the physical device so alt settings are
  actually applied (critical for audio streaming)
- Round up interrupt IN buffers to max_packet_size (matching bulk IN)
- Map USB errors to proper Linux URB status codes: STALL→EPIPE(-32),
  cancelled→ENOENT(-2), timeout→ETIMEDOUT(-110)
- Propagate UrbResponse.status instead of hardcoding 0

Fixes USB Audio Class 2 device detection (kernel error "Audio class
v2/v3 interfaces need an interface association") and device reset
loops caused by EPROTO being returned for benign STALL/cancel errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-22 12:40:44 +00:00
parent 02c3017679
commit 85146a12fe
3 changed files with 84 additions and 11 deletions

View file

@ -402,6 +402,17 @@ impl UsbDevice {
}
Some(BOS) => {
debug!("Get BOS descriptor");
if self.device_handler.is_some() {
let lock = self.device_handler.as_ref().unwrap();
let mut handler = lock.lock().unwrap();
return handler.handle_urb(UrbRequest {
ep,
transfer_buffer_length,
setup: setup_packet,
data: out_data,
..Default::default()
});
}
let mut desc = vec![
0x05, // bLength
BOS as u8, // bDescriptorType: BOS
@ -417,6 +428,19 @@ impl UsbDevice {
}
Some(Configuration) => {
debug!("Get configuration descriptor");
// In passthrough mode, forward to the real device so that
// IADs, class-specific descriptors, etc. are preserved.
if self.device_handler.is_some() {
let lock = self.device_handler.as_ref().unwrap();
let mut handler = lock.lock().unwrap();
return handler.handle_urb(UrbRequest {
ep,
transfer_buffer_length,
setup: setup_packet,
data: out_data,
..Default::default()
});
}
// Standard Configuration Descriptor
let mut desc = vec![
0x09, // bLength
@ -528,6 +552,17 @@ impl UsbDevice {
}
Some(DeviceQualifier) => {
debug!("Get device qualifier descriptor");
if self.device_handler.is_some() {
let lock = self.device_handler.as_ref().unwrap();
let mut handler = lock.lock().unwrap();
return handler.handle_urb(UrbRequest {
ep,
transfer_buffer_length,
setup: setup_packet,
data: out_data,
..Default::default()
});
}
// Device_Qualifier Descriptor
let bcd_usb_qual = self.usb_version.to_bcd_be();
let mut desc = vec![
@ -642,6 +677,17 @@ impl UsbDevice {
(0b00000001, Some(SetInterface)) => {
let intf_index = setup_packet.index as usize & 0xFF;
let alt = setup_packet.value as u8;
// Forward to physical device handler if present
if let Some(ref handler_lock) = self.device_handler {
let mut handler = handler_lock.lock().unwrap();
handler.handle_urb(UrbRequest {
ep,
transfer_buffer_length,
setup: setup_packet.clone(),
data: out_data.clone(),
..Default::default()
})?;
}
match self.interface_states.get(intf_index) {
Some(state) => {
let mut inner = state.inner.write().await;

View file

@ -305,13 +305,15 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
// interrupt
if let Direction::In = ep.direction() {
// interrupt in
// interrupt in - round up to max_packet_size as required by USB spec
let mps = ep.max_packet_size.max(1) as usize;
let alloc_len = ((transfer_buffer_length as usize) + mps - 1) / mps * mps;
let mut endpoint = handle
.endpoint::<nusb::transfer::Interrupt, nusb::transfer::In>(ep.address)
.map_err(|e| {
std::io::Error::other(format!("Failed to open interrupt endpoint: {}", e))
})?;
let buffer = endpoint.allocate(transfer_buffer_length as usize);
let buffer = endpoint.allocate(alloc_len);
let completion = endpoint.transfer_blocking(buffer, timeout);
completion.status.map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other, format!("USB interrupt IN failed: {e}"))

View file

@ -540,6 +540,7 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
match resp {
Ok(urb_resp) => {
let status = urb_resp.status as u32;
if out {
trace!("<-Wrote {transfer_buffer_length}");
// For OUT transfers, actual_length reflects
@ -547,7 +548,7 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
// is not sent back on the wire.
UsbIpResponse::UsbIpRetSubmit {
header: header.clone(),
status: 0,
status,
actual_length: transfer_buffer_length,
start_frame: urb_resp.start_frame,
number_of_packets: urb_resp.iso_packet_descriptors.len() as u32,
@ -557,17 +558,41 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
}
} else {
trace!("<-Resp len={}", urb_resp.data.len());
UsbIpResponse::usbip_ret_submit_success(
&header,
urb_resp.start_frame,
urb_resp.data,
urb_resp.iso_packet_descriptors,
)
UsbIpResponse::UsbIpRetSubmit {
header: header.clone(),
status,
actual_length: urb_resp.data.len() as u32,
start_frame: urb_resp.start_frame,
number_of_packets: urb_resp.iso_packet_descriptors.len() as u32,
error_count: urb_resp.error_count,
transfer_buffer: urb_resp.data,
iso_packet_descriptors: urb_resp.iso_packet_descriptors,
}
}
}
Err(err) => {
warn!("Error handling URB seqnum={seqnum}: {err}");
UsbIpResponse::usbip_ret_submit_fail(&header)
let msg = err.to_string();
// Map USB errors to appropriate Linux URB status codes
let status = if msg.contains("stall") {
(-32i32) as u32 // EPIPE: endpoint stalled
} else if msg.contains("cancel") {
(-2i32) as u32 // ENOENT: URB was unlinked/cancelled
} else if msg.contains("timed out") || msg.contains("TimedOut") {
(-110i32) as u32 // ETIMEDOUT
} else {
(-71i32) as u32 // EPROTO: protocol error
};
warn!("Error handling URB seqnum={seqnum}: {err} (status={status})", status = status as i32);
UsbIpResponse::UsbIpRetSubmit {
header: header.clone(),
status,
actual_length: 0,
start_frame: 0,
number_of_packets: 0,
error_count: 0,
transfer_buffer: vec![],
iso_packet_descriptors: vec![],
}
}
}
}