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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user