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) => { Some(BOS) => {
debug!("Get BOS descriptor"); 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![ let mut desc = vec![
0x05, // bLength 0x05, // bLength
BOS as u8, // bDescriptorType: BOS BOS as u8, // bDescriptorType: BOS
@ -417,6 +428,19 @@ impl UsbDevice {
} }
Some(Configuration) => { Some(Configuration) => {
debug!("Get configuration descriptor"); 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 // Standard Configuration Descriptor
let mut desc = vec![ let mut desc = vec![
0x09, // bLength 0x09, // bLength
@ -528,6 +552,17 @@ impl UsbDevice {
} }
Some(DeviceQualifier) => { Some(DeviceQualifier) => {
debug!("Get device qualifier descriptor"); 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 // Device_Qualifier Descriptor
let bcd_usb_qual = self.usb_version.to_bcd_be(); let bcd_usb_qual = self.usb_version.to_bcd_be();
let mut desc = vec![ let mut desc = vec![
@ -642,6 +677,17 @@ impl UsbDevice {
(0b00000001, Some(SetInterface)) => { (0b00000001, Some(SetInterface)) => {
let intf_index = setup_packet.index as usize & 0xFF; let intf_index = setup_packet.index as usize & 0xFF;
let alt = setup_packet.value as u8; 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) { match self.interface_states.get(intf_index) {
Some(state) => { Some(state) => {
let mut inner = state.inner.write().await; 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 { } else if ep.attributes == EndpointAttributes::Interrupt as u8 {
// interrupt // interrupt
if let Direction::In = ep.direction() { 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 let mut endpoint = handle
.endpoint::<nusb::transfer::Interrupt, nusb::transfer::In>(ep.address) .endpoint::<nusb::transfer::Interrupt, nusb::transfer::In>(ep.address)
.map_err(|e| { .map_err(|e| {
std::io::Error::other(format!("Failed to open interrupt endpoint: {}", 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); let completion = endpoint.transfer_blocking(buffer, timeout);
completion.status.map_err(|e| { completion.status.map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other, format!("USB interrupt IN failed: {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 { match resp {
Ok(urb_resp) => { Ok(urb_resp) => {
let status = urb_resp.status as u32;
if out { if out {
trace!("<-Wrote {transfer_buffer_length}"); trace!("<-Wrote {transfer_buffer_length}");
// For OUT transfers, actual_length reflects // 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. // is not sent back on the wire.
UsbIpResponse::UsbIpRetSubmit { UsbIpResponse::UsbIpRetSubmit {
header: header.clone(), header: header.clone(),
status: 0, status,
actual_length: transfer_buffer_length, actual_length: transfer_buffer_length,
start_frame: urb_resp.start_frame, start_frame: urb_resp.start_frame,
number_of_packets: urb_resp.iso_packet_descriptors.len() as u32, 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 { } else {
trace!("<-Resp len={}", urb_resp.data.len()); trace!("<-Resp len={}", urb_resp.data.len());
UsbIpResponse::usbip_ret_submit_success( UsbIpResponse::UsbIpRetSubmit {
&header, header: header.clone(),
urb_resp.start_frame, status,
urb_resp.data, actual_length: urb_resp.data.len() as u32,
urb_resp.iso_packet_descriptors, 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) => { Err(err) => {
warn!("Error handling URB seqnum={seqnum}: {err}"); let msg = err.to_string();
UsbIpResponse::usbip_ret_submit_fail(&header) // 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![],
}
} }
} }
} }