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.
This commit is contained in:
Red Bear OS
2026-07-18 20:55:41 +09:00
parent 4fdb81fa30
commit ad4bedd46d
+409 -10
View File
@@ -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<const N: usize> Xhci<N> {
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);
}
}