Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ kind: track
opened: 2026-08-10
---

# The BOT/SCSI machine is still hand-written in the kernel
# A disk plugged in after boot is bound inside a scheduling pass

The xHCI port, protocol, teardown, recovery and enumeration machines are pure
crate code with a host simulator behind them, and the kernel drives them. The
mass-storage half is not: the BOT round trip and the SCSI bring-up above it are
still hand-written in the kernel's wait module, and that is the one call site
The BOT round trip and the SCSI bring-up above it are pure machines,
`toyos_xhci::bot::RoundTrip` and `toyos_xhci::scsi::BringUp`, and the kernel
drives both blocking, in place. For the read/write entry points that is the
caller's own time. For the bind it is not: `msc::bind` is the one call site
where a scheduling pass can still spend its transfer budget inside xHCI — for a
disk arriving *after* boot, which is one greppable path.

**What to build**, expressed the way recovery and enumeration already are: the
round trip (command block out, data, status in, one legal stall retry) and the
bring-up above it (test-unit-ready on a budget, sense, inquiry, read-capacity 10
then 16), with two drivers over the same machine — a blocking one for the
read/write entry points and a stepped one for the bind. Blocked on nothing but
its own size; folding it into the enumeration landing would have made that
unreviewable.
**What to build**: a stepped driver for the bind over the same two machines,
one act per pass, as enumeration has. Moving the bind to a thread that may
block — usbd, step 10 of
`issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md` — ends
this file as well.

After it, the pass-duration proof costs no new code: one guest gate measuring a
scheduling pass across a plug, plus the existing check-build's pass-cost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ USB2 half of that receptacle, at speed 3 (high speed):
flash.
- T14 run 75: port 13, slot 5 — the one boot that began with no power cycle of
the stick since run 74's reset had moved it there.
- T14, the `usb_transport_break` boot of PR #588 at 95ebbafdd (2026-09-29):
slot 1, port 1, speed 3, and port 13 did not read connected at the scan. The
scan's own reset of port 1 (0.375 s to 0.430 s) left the stick there; the
port rung's (1.174 s to 1.229 s) did not. That reset's completion read it
Enabled at high speed (`PORTSC 0x00200e03`), port 1 read Disconnected at
1.279 s (`0x000202a0`), and port 13 connected, link already trained, speed 4,
at 2.210 s.

So which half the stick is on at ToyOS's first look is decided before the
driver acts, by something between the firmware's hand-off and the boot scan,
Expand All @@ -28,8 +35,11 @@ lose the volume's disk number
(`issues/kernel/a-disk-taken-offline-is-never-brought-back.md`).

Not investigated. What is not known: what the firmware leaves the two ports in,
whether the boot scan's own reset of port 1 is what keeps the stick there, and
whether the USB3 port reads connected at the scan.
whether the boot scan's own reset of port 1 is what keeps the stick there,
whether the USB3 port reads connected at the scan, and why the port rung's
reset moved the stick where the scan's reset of the same port 0.8 s earlier
did not. xHCI 1.2 says why the USB2 port cannot see the stick leave, not why
the stick leaves: that is the USB 3.x specification's device-side rule.

## Exit condition

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
status: open
kind: defect
opened: 2026-09-30
---

# A bind whose device left aims the ladder at its empty port

Derived from the code, not staged.

`kernel/src/drivers/xhci/wait/msc.rs` has two exits for a device that left its
port. `scsi()` answers a round trip that broke as `Broke::Gone` itself: it
sends no recovery and leaves the disk to its port's teardown. `bring_up` does
not. Its TEST UNIT READY hands every break to `climb_until_in_step`, a `Gone`
one included, with `Left::Elsewhere`. The climb enters at the class reset, whose
steps are aimed at a port that reads empty. Only once that rung has ended does
the climb read the port and find the device gone.

The class rung's bound is spent on a device that is not on the bus, and a
device that left during its bind is handed to the teardown by another path
than one that left during a command. `main` at 3c24b6edb has the same shape:
its bind climbs a `Gone` break too, and the port rung's reset is where it
first reads the port empty.

**Exit**: the port is read once, at the climb's entry as well as after each
rung, so a break whose device left ends there before any rung. `scsi()`'s
`Broke::Gone` arm then goes, and a device that left is one exit.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Two ways a disk that could still be served is lost today:
training SuperSpeed on the other half, so it left port 1 and arrived on port
13. `toyos_xhci::identity` now holds such a disk for `RETURN_WINDOW` and
gives its number back to a device that proves the same identity; QEMU
judges it (`usb_transport_break`'s moved boots), the T14 has not yet.
judges it (`usb_transport_break`'s moved boots).
- **Offline is for the connection's life.** Nothing asks an offline device
again, however long it stays plugged in.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
status: open
kind: defect
opened: 2026-09-30
---

# A USB3 device attached but untrained reads as gone

Derived from the specification and the code, not staged and not seen on the T14.

A USB3 root-hub port that detected a device it could not bring to Enabled
reports it with CAS set and CCS clear (xHCI 1.2 §5.4.8, PORTSC bit 24), and a
warm reset clears it. `toyos_xhci::portsc::Portsc` names no CAS, and nothing
in `toyos-xhci` or `kernel/src/drivers/xhci` warm-resets a port for it. Such a
port reads `!Portsc::holds()`:

- the port machine (`toyos_xhci::port::PortState::step`) keys on CCS, so it
tears down what the port held and never enumerates the device;
- the recovery ladder (`climb` and `reset_port` in
`kernel/src/drivers/xhci/wait/msc.rs`) ends a rung whose stick is in that
state as the stick leaving, and resets nothing.

So a disk whose device lands in that state is lost until it is replugged.
Linux reads CAS as a connection and warm-resets the port
(`xhci_hub_report_usb3_link_state` in `drivers/usb/host/xhci-hub.c`).

**Exit**: `Portsc` names CAS; the port machine warm-resets a USB3 port that
reads it and enumerates what trains; the ladder does not take such a device
for one that left.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Owner ruling, 2026-09-25: the kernel is to be super small, super performant
and safe. This track holds that, and supersedes the ordering of
`issues/kernel/every-wait-in-this-kernel-is-a-spin.md`,
`issues/kernel/every-driver-is-still-in-the-kernel.md` and
`issues/hardware/the-bot-scsi-machine-is-still-hand-written-in-the-kernel.md`,
`issues/hardware/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md`,
which stay as the evidence each stage closes.

The design review of 2026-09-25 read the code and found the same shape three
Expand Down
18 changes: 14 additions & 4 deletions kernel/src/actuator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,22 @@ actuators! {
/// ladder, once: a device that answers nothing on any rung.
usb_reset_break = "usb-reset-break";

/// Hold the port rung's first reset, once, until the port reads empty, so
/// the host can move the device to another port as a reset moved T14 run
/// 79's stick. See `xhci::msc::reset_moves`; judged by
/// `usb_transport_break`.
/// Hold the port rung's first reset, once, until the port reads empty. See
/// `xhci::msc::reset_moves`; judged by `usb_transport_break` and
/// `usb_stick_left`.
usb_reset_moves = "usb-reset-moves";

/// The same hold, once the reset's completion has been read with the
/// device on the port: a device that leaves under a USB2 port's reset.
/// See `xhci::msc::reset_moves`; judged by `usb_stick_left`.
usb_reset_moves_after = "usb-reset-moves-after";

/// The same hold, once the port rung has configured the device again and
/// before its TEST UNIT READY: a device that leaves after every step of
/// the rung was answered. See `xhci::msc::reset_moves`; judged by
/// `usb_stick_left`.
usb_reset_moves_configured = "usb-reset-moves-configured";

/// `usb-transport-break`'s break, on the first WRITE(10) that goes out
/// while its device holds a write it reported complete and no flush has
/// emptied: a device that leaves then may have lost it. Judged by
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/drivers/xhci/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::log;
use toyos_xhci::enumerate::{
self, ep0_packet_from_descriptor, initial_ep0_packet, Act, Enumeration, Learnt, Next, Request,
};
use toyos_xhci::job::{Await, Outcome, Stages};
use toyos_xhci::job::{Await, Outcome, Stages, CC_SUCCESS};
use toyos_xhci::port::{self, Reset};
use toyos_xhci::identity::UsbId;
use toyos_xhci::recovery;
Expand All @@ -14,7 +14,7 @@ use super::{deadline, Answer, Trb, TrbRing, What, XhciController, PAGE};
use super::{OFF_INPUT_CTX, OFF_DATA_BUF};
use super::{DEV_INT_RING, DEV_EP0_RING, DEV_OUT_CTX, DEV_REPORT, EP0_DCI};
use super::{TRB_ENABLE_SLOT, TRB_ADDRESS_DEVICE, TRB_CONFIGURE_EP, TRB_EVALUATE_CONTEXT};
use super::{enqueue_control, CC_SUCCESS};
use super::enqueue_control;

use super::hid::{HidType, HidRole, HidDevice};
use super::msc::{Bind, MscInterface, MscRings};
Expand Down
13 changes: 3 additions & 10 deletions kernel/src/drivers/xhci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::log;
use super::pci::PciDevice;
use crate::sync::Lock;
use toyos_untrusted::Untrusted;
use toyos_xhci::job::{Await, Outcome, Outstanding, Stages};
use toyos_xhci::job::{Await, Outcome, Outstanding, Stages, CC_SHORT_PACKET, CC_SUCCESS};
use toyos_xhci::port::{self as portmachine, GaveUp, Gone, PortState, Reset, Step};
use toyos_xhci::call::AfterBreak;
use toyos_xhci::recovery::{self, Act, EndpointState, NeedsConfigure, Recovery};
Expand Down Expand Up @@ -111,10 +111,6 @@ const EVENT_TRANSFER: u32 = 32;
const EVENT_CMD_COMPLETE: u32 = 33;
const EVENT_PORT_STATUS_CHANGE: u32 = 34;

// CC_SHORT_PACKET is success with a residue, not an error — treating it as one is the classic mass-storage bug.
const CC_SUCCESS: u32 = 1;
const CC_STALL: u32 = 6;
const CC_SHORT_PACKET: u32 = 13;
const CC_CONTEXT_STATE_ERROR: u32 = 19;
/// Stopped, Stopped - Length Invalid and Stopped - Short Packet (Table 6-90): the transfer events a Stop Endpoint raises for the TRB it stopped inside (§4.6.9).
const CC_STOPPED: core::ops::RangeInclusive<u32> = 26..=28;
Expand Down Expand Up @@ -993,11 +989,9 @@ impl XhciController {
(dev.ep_addr, dev.int_ep_dci, dev.port_idx, dev.block);

// Disconnect wins the race: a transaction-error code from a pulled device is indistinguishable from a bad cable, only the port register tells them apart.
//
// CSC as well as CCS: a replug reads connected again but the transfer still died with the old device.
let slot = self.slot(slot_id);
let portsc = self.read_portsc(port_idx);
if !portsc.connected() || portsc.connect_changed() {
if !portsc.holds() {
log!("xHCI: USB {kind} on {slot}: interrupt endpoint {ep_addr:#04x} \
completed with {} as its port went away; leaving it to the disconnect",
Completion(code));
Expand Down Expand Up @@ -1197,8 +1191,7 @@ impl XhciController {
const MAX_EFFECTS: usize = 16;
for _ in 0..MAX_EFFECTS {
let portsc = self.read_portsc(port_idx);
// CCS or CSC: a replug between two looks reads connected again, but the device that was here has still gone.
if !portsc.connected() || portsc.connect_changed() {
if !portsc.holds() {
self.cancel_recovery_on(port_idx);
device::cancel_on(self, port_idx);
}
Expand Down
5 changes: 2 additions & 3 deletions kernel/src/drivers/xhci/wait/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ mod depth_probe {
use crate::log;
use super::{deadline, enqueue_control, log_unrecoverable, Completion, Trb, TrbRing};
use super::{XhciController, EVENT_TRANSFER, EVENT_CMD_COMPLETE, USB_TIMEOUT_NS};
use super::{CC_SUCCESS, CC_SHORT_PACKET};
use toyos_xhci::call::NotTaken;
use toyos_xhci::job::Await;
use toyos_xhci::job::{Await, CC_SHORT_PACKET, CC_STALL, CC_SUCCESS};
use toyos_xhci::recovery::{Act, NeedsConfigure, Recovery};
use toyos_xhci::scan;

Expand Down Expand Up @@ -482,7 +481,7 @@ impl XhciController {
/// Take EP0 back out of Halted where `code` says the device stalled the
/// transfer, before the failure reaches a caller likely to send another one.
fn recover_after(&mut self, slot: u8, ctx_block: usize, ring: &mut TrbRing, code: u32) {
if code != super::CC_STALL {
if code != CC_STALL {
return;
}
if !self.restart_control_endpoint(slot, ctx_block, ring) {
Expand Down
Loading
Loading