From ad4bedd46d5889405a6a91ab71a19a956f1b20cc Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 18 Jul 2026 20:55:41 +0900 Subject: [PATCH] xhcid: expand xHCI completion-code error recovery to all 36 codes maybe_recover_transfer_error previously handled only the first-tier codes (UsbTransaction, Resource, Stall, BabbleDetected, DataBuffer, Trb, SplitTransaction) and silently returned Ok(false) for every other completion code via a catch-all arm. Cross-referenced Linux 7.1 drivers/usb/host/xhci-ring.c handle_tx_event() (line 2608+) and handle_transferless_tx_event() (line 2561+) to add explicit recovery for the remaining ~29 codes: - Stopped/StoppedLengthInvalid/StoppedShortPacket: restart endpoint + retry (up to MAX_SOFT_RETRY), then hard-reset - InvalidStreamType/InvalidStreamId: soft reset + retry, then hard-reset - IncompatibleDevice: disable slot (device must re-enumerate) - MissedService/NoPingResponse: log informational, surface to caller - ContextState/Parameter: hard-reset to resync driver/xHC state - Bandwidth/BandwidthOverrun/SecondaryBandwidth: log, no transfer recovery (config must change) - IsochBuffer: hard-reset endpoint - MaxExitLatencyTooLarge: log, surface - EventLost/Undefined: hard-reset (event ring may be corrupted) - SlotNotEnabled/EndpointNotEnabled/NoSlotsAvailable: log driver state mismatch - CommandRingStopped/CommandAborted: log xHC state confusion - Reserved/vendor: default arm logs explicitly Added completion_code_to_errno() mapping transfer completion codes to POSIX errnos matching Linux 7.1 semantics: Stall -> EPIPE BabbleDetected -> EOVERFLOW UsbTransaction/SplitTransaction/IncompatibleDevice -> EPROTO Trb -> EILSEQ DataBuffer -> ENOSR SlotNotEnabled/EndpointNotEnabled/NoSlotsAvailable -> ENODEV default -> EIO Rewrote handle_transfer_event_trb() to use the new errno mapping instead of always returning EIO. Added 14 unit tests covering all errno mappings and transfer event handling paths. Full suite (41 tests) passes. --- drivers/usb/xhcid/src/xhci/scheme.rs | 419 ++++++++++++++++++++++++++- 1 file changed, 409 insertions(+), 10 deletions(-) diff --git a/drivers/usb/xhcid/src/xhci/scheme.rs b/drivers/usb/xhcid/src/xhci/scheme.rs index b11983db79..f6bb9a30b2 100644 --- a/drivers/usb/xhcid/src/xhci/scheme.rs +++ b/drivers/usb/xhcid/src/xhci/scheme.rs @@ -33,9 +33,9 @@ use common::io::Io; use redox_scheme::{CallerCtx, OpenResult}; use syscall::schemev2::NewFdFlags; use syscall::{ - Error, Result, Stat, EACCES, EBADF, EBADFD, EBADMSG, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, - ENOTDIR, EOPNOTSUPP, EPROTO, ESPIPE, MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, O_RDWR, - O_STAT, O_WRONLY, SEEK_CUR, SEEK_END, SEEK_SET, + Error, Result, Stat, EACCES, EBADF, EBADFD, EBADMSG, EILSEQ, EINVAL, EIO, EISDIR, ENODEV, + ENOENT, ENOSR, ENOSYS, ENOTDIR, EOPNOTSUPP, EOVERFLOW, EPIPE, EPROTO, ESPIPE, MODE_CHR, + MODE_DIR, MODE_FILE, O_DIRECTORY, O_RDWR, O_STAT, O_WRONLY, SEEK_CUR, SEEK_END, SEEK_SET, }; use super::{port, usb}; @@ -710,7 +710,217 @@ impl Xhci { hard_reset_endpoint.await?; Ok(false) } - _ => Ok(false), + + // ── Endpoint stopped codes (Linux 7.1 xhci-ring.c:2666-2679) ── + // Not errors: endpoint was stopped before transfer completed. + // Linux treats as status=-EINPROGRESS; restart + retry here. + x if x == C::Stopped as u8 + || x == C::StoppedLengthInvalid as u8 + || x == C::StoppedShortPacket as u8 => + { + if *retry_count < Self::MAX_SOFT_RETRY { + *retry_count += 1; + warn!( + "{}: endpoint stopped (cc={:02X}) on port {} ep {} — restart + retry {}/{}", + context, + completion_code, + port_num, + endp_num, + *retry_count, + Self::MAX_SOFT_RETRY + ); + if !control_path { + self.restart_endpoint(port_num, endp_num).await?; + } else { + self.reset_port(port_num)?; + } + return Ok(true); + } + warn!( + "{}: endpoint stopped (cc={:02X}) on port {} ep {} — retry budget exhausted", + context, + completion_code, + port_num, + endp_num + ); + hard_reset_endpoint.await?; + Ok(false) + } + + // ── Stream errors (Linux 7.1 handle_transferless_tx_event:2567-2568) ── + // Soft reset with err_count, escalate to hard reset. + x if x == C::InvalidStreamType as u8 || x == C::InvalidStreamId as u8 => { + if !control_path && *retry_count < Self::MAX_SOFT_RETRY { + *retry_count += 1; + warn!( + "{}: stream error (cc={:02X}) on port {} ep {} — soft reset + retry {}/{}", + context, + completion_code, + port_num, + endp_num, + *retry_count, + Self::MAX_SOFT_RETRY + ); + self.reset_endpoint(port_num, endp_num, true).await?; + self.restart_endpoint(port_num, endp_num).await?; + return Ok(true); + } + warn!( + "{}: stream error (cc={:02X}) on port {} ep {} — hard reset", + context, + completion_code, + port_num, + endp_num + ); + hard_reset_endpoint.await?; + Ok(false) + } + + // ── Incompatible device (Linux 7.1 xhci-ring.c:2757-2763) ── + // Needs disable slot to recover; device must re-enumerate. + x if x == C::IncompatibleDevice as u8 => { + error!( + "{}: incompatible device on port {} ep {} — disabling slot", + context, + port_num, + endp_num + ); + if let Some(state) = self.port_states.get(&port_num) { + let slot = state.slot; + if let Err(e) = self.disable_port_slot(slot).await { + warn!( + "{}: disable_slot failed for incompatible device on port {} slot {}: {}", + context, + port_num, + slot, + e + ); + } + } + Ok(false) + } + + // ── Isochronous informational (Linux 7.1 xhci-ring.c:2738-2755) ── + // MissedService: Linux sets ep->skip=true. NoPingResponse: same. + // We lack a skip mechanism; log and surface. + x if x == C::MissedService as u8 || x == C::NoPingResponse as u8 => { + warn!( + "{}: isochronous service event (cc={:02X}) on port {} ep {} — transfer may be incomplete", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } + + // ── Context/parameter errors (Linux 7.1 xhci-ring.c:2702-2714) ── + // Driver/xHC state mismatch. Hard-reset to resynchronize. + x if x == C::ContextState as u8 || x == C::Parameter as u8 => { + error!( + "{}: context/parameter error (cc={:02X}) on port {} ep {} — hard reset to resync", + context, + completion_code, + port_num, + endp_num + ); + hard_reset_endpoint.await?; + Ok(false) + } + + // ── Bandwidth errors (Linux 7.1 xhci-ring.c:2715-2719) ── + // No transfer-level recovery; device config must change. + x if x == C::BandwidthOverrun as u8 + || x == C::Bandwidth as u8 + || x == C::SecondaryBandwidth as u8 => + { + error!( + "{}: bandwidth error (cc={:02X}) on port {} ep {} — no transfer-level recovery", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } + + // ── Isochronous buffer overrun (Linux 7.1 xhci-ring.c:2720-2724) ── + x if x == C::IsochBuffer as u8 => { + error!( + "{}: isoch buffer overrun (cc={:02X}) on port {} ep {}", + context, + completion_code, + port_num, + endp_num + ); + hard_reset_endpoint.await?; + Ok(false) + } + + // ── Max exit latency (Linux 7.1 COMP_MAX_EXIT_LATENCY_TOO_LARGE) ── + x if x == C::MaxExitLatencyTooLarge as u8 => { + error!( + "{}: max exit latency too large (cc={:02X}) on port {} ep {}", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } + + // ── Event lost / undefined (Linux 7.1 default case) ── + // Fatal for in-flight transfers. + x if x == C::EventLost as u8 || x == C::Undefined as u8 => { + error!( + "{}: fatal event error (cc={:02X}) on port {} ep {} — event ring may be corrupted", + context, + completion_code, + port_num, + endp_num + ); + hard_reset_endpoint.await?; + Ok(false) + } + + // ── Slot/endpoint not enabled — driver state mismatch ── + x if x == C::SlotNotEnabled as u8 + || x == C::EndpointNotEnabled as u8 + || x == C::NoSlotsAvailable as u8 => + { + error!( + "{}: slot/endpoint state error (cc={:02X}) on port {} ep {} — driver state mismatch", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } + + // ── Command ring codes on a transfer event — xHC state confusion ── + x if x == C::CommandRingStopped as u8 || x == C::CommandAborted as u8 => { + error!( + "{}: command-ring code (cc={:02X}) on transfer event for port {} ep {} — xHC state confusion", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } + + // ── Vendor-defined/reserved (Linux 7.1 default + is_vendor_info_code) ── + _ => { + error!( + "{}: unhandled completion code {:02X} on port {} ep {}", + context, + completion_code, + port_num, + endp_num + ); + Ok(false) + } } } @@ -3247,17 +3457,49 @@ pub fn handle_event_trb(name: &str, event_trb: &Trb, command_trb: &Trb) -> Resul Err(Error::new(EIO)) } } + +/// Map an xHCI completion code to a Redox errno, matching the status +/// mapping in Linux 7.1 xhci-ring.c handle_tx_event() (lines 2656-2763). +/// Success and ShortPacket map to 0 (no error). +pub fn completion_code_to_errno(cc: u8) -> i32 { + use TrbCompletionCode as C; + match cc { + x if x == C::Success as u8 || x == C::ShortPacket as u8 => 0, + x if x == C::Stall as u8 => EPIPE, + x if x == C::BabbleDetected as u8 => EOVERFLOW, + x if x == C::UsbTransaction as u8 + || x == C::SplitTransaction as u8 + || x == C::IncompatibleDevice as u8 => + { + EPROTO + } + x if x == C::Trb as u8 => EILSEQ, + x if x == C::DataBuffer as u8 => ENOSR, + x if x == C::SlotNotEnabled as u8 + || x == C::EndpointNotEnabled as u8 + || x == C::NoSlotsAvailable as u8 => + { + ENODEV + } + _ => EIO, + } +} + pub fn handle_transfer_event_trb(name: &str, event_trb: &Trb, transfer_trb: &Trb) -> Result<()> { - if event_trb.completion_code() == TrbCompletionCode::Success as u8 - || event_trb.completion_code() == TrbCompletionCode::ShortPacket as u8 - { + let cc = event_trb.completion_code(); + let errno = completion_code_to_errno(cc); + if errno == 0 { Ok(()) } else { error!( - "{} transfer {:?} failed with event {:?}", - name, transfer_trb, event_trb + "{} transfer {:?} failed with cc={:02X} (errno {}) event {:?}", + name, + transfer_trb, + cc, + errno, + event_trb ); - Err(Error::new(EIO)) + Err(Error::new(errno)) } } use lazy_static::lazy_static; @@ -3273,3 +3515,160 @@ where a / b } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn errno_mapping_success_and_short_are_zero() { + assert_eq!(completion_code_to_errno(TrbCompletionCode::Success as u8), 0); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::ShortPacket as u8), + 0 + ); + } + + #[test] + fn errno_mapping_stall_is_epipe() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::Stall as u8), + EPIPE + ); + } + + #[test] + fn errno_mapping_babble_is_eoverflow() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::BabbleDetected as u8), + EOVERFLOW + ); + } + + #[test] + fn errno_mapping_transaction_split_incompatible_are_eproto() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::UsbTransaction as u8), + EPROTO + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::SplitTransaction as u8), + EPROTO + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::IncompatibleDevice as u8), + EPROTO + ); + } + + #[test] + fn errno_mapping_trb_is_eilseq() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::Trb as u8), + EILSEQ + ); + } + + #[test] + fn errno_mapping_data_buffer_is_enosr() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::DataBuffer as u8), + ENOSR + ); + } + + #[test] + fn errno_mapping_slot_endpoint_errors_are_enodev() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::SlotNotEnabled as u8), + ENODEV + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::EndpointNotEnabled as u8), + ENODEV + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::NoSlotsAvailable as u8), + ENODEV + ); + } + + #[test] + fn errno_mapping_unknown_falls_back_to_eio() { + assert_eq!( + completion_code_to_errno(TrbCompletionCode::Bandwidth as u8), + EIO + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::Resource as u8), + EIO + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::Stopped as u8), + EIO + ); + assert_eq!( + completion_code_to_errno(TrbCompletionCode::MissedService as u8), + EIO + ); + assert_eq!( + completion_code_to_errno(0xFF), + EIO + ); + } + + #[test] + fn errno_mapping_never_panics_on_any_byte() { + for cc in 0u8..=255u8 { + let _ = completion_code_to_errno(cc); + } + } + + fn make_transfer_trb(cc: u8) -> Trb { + use common::io::Mmio; + Trb { + data_low: Mmio::new(0), + data_high: Mmio::new(0), + status: Mmio::new((u32::from(cc)) << 24), + control: Mmio::new((TrbType::Transfer as u32) << 10 | 1), + } + } + + #[test] + fn handle_transfer_success_returns_ok() { + let event = make_transfer_trb(TrbCompletionCode::Success as u8); + let transfer = make_transfer_trb(0); + assert!(handle_transfer_event_trb("TEST", &event, &transfer).is_ok()); + } + + #[test] + fn handle_transfer_short_packet_returns_ok() { + let event = make_transfer_trb(TrbCompletionCode::ShortPacket as u8); + let transfer = make_transfer_trb(0); + assert!(handle_transfer_event_trb("TEST", &event, &transfer).is_ok()); + } + + #[test] + fn handle_transfer_stall_returns_epipe() { + let event = make_transfer_trb(TrbCompletionCode::Stall as u8); + let transfer = make_transfer_trb(0); + let err = handle_transfer_event_trb("TEST", &event, &transfer).unwrap_err(); + assert_eq!(err.errno, EPIPE); + } + + #[test] + fn handle_transfer_babble_returns_eoverflow() { + let event = make_transfer_trb(TrbCompletionCode::BabbleDetected as u8); + let transfer = make_transfer_trb(0); + let err = handle_transfer_event_trb("TEST", &event, &transfer).unwrap_err(); + assert_eq!(err.errno, EOVERFLOW); + } + + #[test] + fn handle_transfer_trb_error_returns_eilseq() { + let event = make_transfer_trb(TrbCompletionCode::Trb as u8); + let transfer = make_transfer_trb(0); + let err = handle_transfer_event_trb("TEST", &event, &transfer).unwrap_err(); + assert_eq!(err.errno, EILSEQ); + } +}