Repository navigation
USB mass storage: the BOT round trip and SCSI bring-up are toyos-xhci machines, and a stick that leaves its port is no break - #588
Conversation
…xhci machines
The Bulk-Only round trip and the SCSI above it were the one half of the xHCI
driver still hand-written in the kernel's wait module. Every decision in them
moves into toyos-xhci as pure code with host tests; kernel/src/drivers/xhci/
wait/msc.rs keeps the transfers, the waits, the phase publishing, the staged
actuators and the log lines, so a userland usbd can drive the same machines.
toyos_xhci::bot
- `cbw`: the 31-byte CBW (BOT 1.0 §5.1), LUN 0, direction from the CDB.
- `RoundTrip`: which transfer is owed (CBW, data, CSW), what each completion
means, a stalled data phase restarted and its status read (§6.7.2), the one
legal status-stall retry (§5.3.3), and `CswDue::judge`, the CSW believed
only when valid and meaningful (§6.3), delivered = min(controller, device).
- `Broke<W>` with `left` and `event`, generic over the driver's silence reason.
A disconnect is its own variant, `Gone`, where it was `Silence { why:
Quiet::Gone }`: the crate has to tell it apart and the kernel's `Quiet` is
not the crate's. `Broke::left` matches on a `Phase`, not on a phase's name.
toyos_xhci::scsi
- `Cdb`, built only by this module and carrying its own direction, so no
caller passes a length or a direction beside it (the `cdb_len`/`data_in`
parameters and the kernel's shape assert are gone).
- `Sense` (fourteen bytes or none), `Outcome`, `flushed` (INVALID COMMAND
OPERATION CODE is no cache, not a failure), `Transfer` (READ/WRITE(10)
batching, and only the first batch may answer "ask again"), and `BringUp`:
TEST UNIT READY on a budget, sense, recovery, INQUIRY, READ CAPACITY(10)
then (16), and every refusal by name — a stepped machine, driven blocking
by the kernel as enumeration's boot scan drives its own.
toyos_xhci::ladder::Run holds a device's break count and highest rung;
identity::first_language reads descriptor zero's LANGID.
No behaviour change on the wire: the same commands, bytes, order and waits.
Every log line renders as before, and the two spellings toyos-blackbox holds
to the source are kept. What differs off the wire: the CBW is one 31-byte copy
where it was field writes through `Unaligned` (same bytes, same offset) and
the CSW one 13-byte copy; REQUEST SENSE's response is copied whenever the
round trip completed and discarded under 14 bytes; bring-up copies the whole
allocation rather than the bytes it reads; a status-stall retry publishes
`StatusOwed` twice (the same value); `Transfer::next` panics where the old
code truncated a sector number bring-up and the range check already make
unreachable; and `msc_flush`'s inner latch check, always true, is gone.
sourcegate: the `: u32 = 4096` exception for msc.rs goes with HOST_BLOCK,
which is the crate's now.
The issue's slug claimed the machine is hand-written in the kernel, which this
refutes; what it still owes is the bind, the one scheduling-pass call site, so
it is renamed to
issues/hardware/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md
and both citations move with it.
kernel/src: 64984 -> 64697 lines (find kernel/src -name '*.rs' | xargs cat |
wc -l); msc.rs 2647 -> 2360.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 1, at 627a715CI Growth, from BLOCKER
NOTE
REMOVE
SEND BACK |
…ne seam exercised - CC_STALL lives in toyos_xhci::job beside CC_SUCCESS and CC_SHORT_PACKET; the kernel's EP0 recovery imports it and its own copy is deleted. - bot: two walks on the data-stall route. A Data STALL leaving 100 B unmoved then CSW (TAG, 0, 0) delivers 412 B; a Data STALL, a Status STALL and a whole CSW asks the status twice and ends Ok. A controller residue larger than the transfer delivers nothing, on both routes. - bot: the CSW is judged in BOT 1.0 §6.3.2's order: status 2 is a phase error whatever the residue, 0 and 1 are refused for a residue past the transfer, and 3..=255 are Broke::Reserved, no longer a phase error. - msc.rs: the staged PortGone is the Command act's completion, completed(Err(Quiet::Gone)), so the actuator goes through the arm that sends a disconnect to the teardown rather than past it. - scsi: the crate's Nanos, not a fifth alias; Geometry's fields are private behind accessors, and a test pins that Geometry::NONE fits no transfer; Outcome is Reply, apart from the crate root's job::Outcome; the refusal of 256-byte sectors is stated as this driver's set, not SBC-3's. - Removed: msc.rs's "every decision is the crate's" paragraph, scsi.rs's "driven in place today" paragraph, SectorSize's doc and Broke::Csw's doc. Mutations, each a checked patch built, run and reversed (cargo test -p toyos-xhci EXIT=101 on each): stall moved = data_len; stall status(true); data residue wrapping_sub; the CSW's old residue-first order; stall residue wrapping_sub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 2, at f90b2d2CI Growth, from Round-1 BLOCKERs
The redlist
Mutation patches
BLOCKER
NOTE
REMOVE
SEND BACK |
src/sourcegate.rs takes main's side. The branch's only hunk removed the msc.rs row from the `: u32 = 4096` ban, and main deleted the Ban table itself in #624; the 4096 rule is the reviewer's now, and HOST_BLOCK no longer lives in the kernel. issues/build/the-swarm-is-not-yet-falsifiable.md is deleted, as main deleted it. The branch's only hunk there swapped the citation of the renamed issue inside that document. The new slug is still cited from issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md, and the old slug is cited nowhere, by the bare name as well as the path. rust takes main's pin, 90697f14, and the fork checkout is moved to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both were declared in the driver's mod.rs as well as in the crate, so the kernel's copy answered the crate's machine in msc.rs. The crate's job.rs is the one declaration, as it already is for CC_STALL, and every consumer imports from it. The driver's comment on the pair goes with the constants: the crate's doc on them already says that Short Packet is a success with a residue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutation patches at 95ebbafThe patches behind the negative-control table in the description, exactly as applied. Each is one hunk against m01-cbw-direction-inverted--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -116,7 +116,7 @@
out[0..4].copy_from_slice(&CBW_SIGNATURE.to_le_bytes());
out[4..8].copy_from_slice(&tag.to_le_bytes());
out[8..12].copy_from_slice(&data_len.to_le_bytes());
- out[12] = if cdb.data_in() { 0x80 } else { 0x00 };
+ out[12] = if cdb.data_in() { 0x00 } else { 0x80 };
let bytes = cdb.bytes();
out[14] = bytes.len() as u8;
out[15..15 + bytes.len()].copy_from_slice(bytes);m02-csw-tag-not-checked--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -348,7 +348,7 @@
}
// A mismatched tag would attribute one command's status to another — a
// write reporting the read before it as success.
- if tag != self.tag {
+ if tag != self.tag && false {
return Err(Broke::Csw { what: "tag", got: tag, want: self.tag, status, residue });
}
match status {m03-delivered-trusts-the-device-alone--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -356,7 +356,7 @@
0 | 1 if residue > self.data_len => Err(Broke::Residue { unmoved: residue, of: self.data_len }),
// Neither account is trusted alone: a caller may read only what
// both the device and the controller say arrived.
- 0 => Ok(Bot::Done { delivered: self.moved.min(self.data_len - residue) }),
+ 0 => Ok(Bot::Done { delivered: self.data_len - residue }),
1 => Ok(Bot::Failed),
status => Err(Broke::Reserved { status }),
}m04-status-stall-retried-without-bound--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -295,7 +295,7 @@
(At::DataRestart { .. }, Answer::Restarted(false)) => Next::Broke(Broke::Stall { phase: Phase::Data }),
// The class's one legal retry: a device may STALL the status
// phase once (§5.3.3, Figure 2).
- (At::Status { retried: false }, Answer::Moved { code: CC_STALL, .. }) => Next::Act(
+ (At::Status { .. }, Answer::Moved { code: CC_STALL, .. }) => Next::Act(
Self { at: At::StatusRestart, ..self },
Act::Restart { pipe: Pipe::In, then: Phase::StatusOwed },
),m05-csw-residue-unbounded--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -353,7 +353,7 @@
}
match status {
2 => Err(Broke::PhaseError),
- 0 | 1 if residue > self.data_len => Err(Broke::Residue { unmoved: residue, of: self.data_len }),
+ 0 | 1 if residue > self.data_len && false => Err(Broke::Residue { unmoved: residue, of: self.data_len }),
// Neither account is trusted alone: a caller may read only what
// both the device and the controller say arrived.
0 => Ok(Bot::Done { delivered: self.moved.min(self.data_len - residue) }),m06-short-cbw-or-csw-accepted--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -318,7 +318,7 @@
/// CSW on a 512-byte endpoint — so only the residue says it all moved.
fn framed<W>(phase: Phase, pipe: Pipe, len: u32, answer: Answer<W>) -> Result<(), Broke<W>> {
match answer {
- Answer::Moved { code: CC_SUCCESS | CC_SHORT_PACKET, residue: 0 } => Ok(()),
+ Answer::Moved { code: CC_SUCCESS | CC_SHORT_PACKET, .. } => Ok(()),
Answer::Moved { code: CC_SUCCESS | CC_SHORT_PACKET, residue } => {
Err(Broke::Short { phase, moved: len.saturating_sub(residue), wanted: len })
}m07-data-stall-not-restarted--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -279,7 +279,7 @@
// A stalled data phase is ordinary — an unsupported command, a read
// past the end — and the status that follows its recovery turns it
// into a clean refusal (§6.7.2).
- (At::Data, Answer::Moved { code: CC_STALL, residue }) => Next::Act(
+ (At::Data, Answer::Moved { code: CC_STALL, residue }) if false => Next::Act(
Self { at: At::DataRestart { unmoved: residue }, ..self },
Act::Restart { pipe: data_pipe, then: Phase::Data },
),m08-sense-believed-at-13-bytes--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -92,7 +92,7 @@
/// The sense `response` carries, of which `delivered` bytes arrived. ASCQ
/// is byte 13, so fourteen must have or none of it is believed.
pub fn of(response: &[u8; SENSE_BYTES], delivered: u32) -> Self {
- if delivered < 14 {
+ if delivered < 13 {
return Self::NONE;
}
Self { key: response[2] & 0x0F, asc: response[12], ascq: response[13] }m09-ask-again-after-blocks-moved--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -257,7 +257,7 @@
}
Reply::Ok { delivered } => Moved::Short { delivered },
Reply::Refused(sense) => Moved::Refused(sense),
- Reply::Budget if first => Moved::Ended(Fail::Budget),
+ Reply::Budget => Moved::Ended(Fail::Budget),
Reply::Broken | Reply::Budget => Moved::Ended(Fail::Device),
}
}m10-read-capacity-16-never-asked--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -487,7 +487,7 @@
Next::Disk(Self { at: At::Read(next), ..self }, Inquiry(said), Ask::Read(next))
}
// An all-ones last LBA says the disk needs the 16-byte form.
- Query::Capacity10 if be32(0) == u32::MAX => {
+ Query::Capacity10 if be32(0) == u32::MAX && false => {
self.ask(At::Read(Query::Capacity16), Ask::Read(Query::Capacity16))
}
Query::Capacity10 => Next::Up(geometry(u64::from(be32(0)), be32(4))),m11-disk-past-read-10-served--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -506,7 +506,7 @@
if !matches!(sector_bytes, 512 | 1024 | 2048 | 4096) {
return Up::Refused(Refusal::SectorSize(sector_bytes));
}
- if last_lba > u64::from(u32::MAX) {
+ if last_lba > u64::from(u32::MAX) && false {
return Up::Refused(Refusal::PastRead10 { last_lba });
}
let sectors = last_lba + 1;m12-no-cache-flush-is-a-failure--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -277,7 +277,7 @@
pub fn flushed(reply: Reply) -> Flushed {
match reply {
- Reply::Refused(sense) if sense.unimplemented() => Flushed::NoCache,
+ Reply::Refused(sense) if sense.unimplemented() && false => Flushed::NoCache,
Reply::Ok { .. } => Flushed::Emptied,
Reply::Refused(sense) => Flushed::Refused(sense),
Reply::Broken => Flushed::Ended(Fail::Device),m13-ready-budget-ignored--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -442,7 +442,7 @@
/// Another attempt, if the budget has room for one at `now`.
fn again(self, now: Nanos, offline: bool) -> Next {
- if offline || now >= self.give_up {
+ if offline {
return Next::Up(Up::Unready { sense: self.sense, offline });
}
self.ask(At::Ready, Ask::TestUnitReady)m14-sector-lba-unscaled--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -231,7 +231,7 @@
}
let blocks = (self.count - self.done).min(self.most);
let block = self.lba + u64::from(self.done);
- let sector = block * u64::from(self.sectors_per_block);
+ let sector = block;
// `BringUp` refused a disk whose last sector does not fit 32 bits, and
// `new` refused a transfer past the disk.
let sector = u32::try_from(sector).expect("a sector past what READ(10) addresses");m15-peripheral-qualifier-read-as-the-type--- a/toyos-xhci/src/scsi.rs
+++ b/toyos-xhci/src/scsi.rs
@@ -477,7 +477,7 @@
let be32 = |at: usize| u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
match query {
Query::Inquiry => {
- let peripheral = bytes[0] & 0x1F;
+ let peripheral = bytes[0];
if peripheral != 0 {
return Next::Up(Up::Refused(Refusal::NotADisk(peripheral)));
}m16-run-over-keeps-its-rung--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -138,7 +138,9 @@
/// A round trip completed: the run is over. How many breaks it held.
pub fn over(&mut self) -> u8 {
- core::mem::replace(self, Self::NONE).breaks
+ let breaks = self.breaks;
+ self.breaks = 0;
+ breaks
}
}
m17-skip-class-reset-said-on-every-rung--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -131,7 +131,7 @@
pub fn broke(&mut self, left: Left) -> Climb {
self.breaks = self.breaks.saturating_add(1);
let rung = next(self.climbed, left);
- let skips_class_reset = self.climbed.is_none() && rung != Rung::ClassReset;
+ let skips_class_reset = rung != Rung::ClassReset;
self.climbed = Some(rung);
Climb { rung, skips_class_reset }
}m18-langid-read-from-an-empty-descriptor--- a/toyos-xhci/src/identity.rs
+++ b/toyos-xhci/src/identity.rs
@@ -102,7 +102,7 @@
/// arrived; a device offering none names no string.
pub fn first_language(arrived: &[u8]) -> Option<u16> {
match *arrived {
- [length, 3, lo, hi, ..] if length >= 4 => Some(u16::from_le_bytes([lo, hi])),
+ [_length, 3, lo, hi, ..] => Some(u16::from_le_bytes([lo, hi])),
_ => None,
}
}m19-stall-delivers-the-devices-residue-alone--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -289,7 +289,7 @@
(At::Data, Answer::Silent(why)) => Next::Broke(Broke::Silence { phase: Phase::Data, why }),
(At::Data, Answer::Gone) => Next::Broke(Broke::Gone { phase: Phase::Data }),
(At::DataRestart { unmoved }, Answer::Restarted(true)) => {
- let (trip, act) = Self { moved: self.data_len.saturating_sub(unmoved), ..self }.status(false);
+ let (trip, act) = Self { moved: self.data_len, ..self }.status(false);
Next::Act(trip, act)
}
(At::DataRestart { .. }, Answer::Restarted(false)) => Next::Broke(Broke::Stall { phase: Phase::Data }),m20-data-stall-then-no-status-retry--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -289,7 +289,7 @@
(At::Data, Answer::Silent(why)) => Next::Broke(Broke::Silence { phase: Phase::Data, why }),
(At::Data, Answer::Gone) => Next::Broke(Broke::Gone { phase: Phase::Data }),
(At::DataRestart { unmoved }, Answer::Restarted(true)) => {
- let (trip, act) = Self { moved: self.data_len.saturating_sub(unmoved), ..self }.status(false);
+ let (trip, act) = Self { moved: self.data_len.saturating_sub(unmoved), ..self }.status(true);
Next::Act(trip, act)
}
(At::DataRestart { .. }, Answer::Restarted(false)) => Next::Broke(Broke::Stall { phase: Phase::Data }),m21-data-residue-wrapping-sub--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -273,7 +273,7 @@
}
},
(At::Data, Answer::Moved { code: CC_SUCCESS | CC_SHORT_PACKET, residue }) => {
- let (trip, act) = Self { moved: self.data_len.saturating_sub(residue), ..self }.status(false);
+ let (trip, act) = Self { moved: self.data_len.wrapping_sub(residue), ..self }.status(false);
Next::Act(trip, act)
}
// A stalled data phase is ordinary — an unsupported command, a readm22-csw-residue-checked-before-status-2--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -352,8 +352,8 @@
return Err(Broke::Csw { what: "tag", got: tag, want: self.tag, status, residue });
}
match status {
+ _ if residue > self.data_len => Err(Broke::Residue { unmoved: residue, of: self.data_len }),
2 => Err(Broke::PhaseError),
- 0 | 1 if residue > self.data_len => Err(Broke::Residue { unmoved: residue, of: self.data_len }),
// Neither account is trusted alone: a caller may read only what
// both the device and the controller say arrived.
0 => Ok(Bot::Done { delivered: self.moved.min(self.data_len - residue) }),m23-data-stall-residue-wrapping-sub--- a/toyos-xhci/src/bot.rs
+++ b/toyos-xhci/src/bot.rs
@@ -289,7 +289,7 @@
(At::Data, Answer::Silent(why)) => Next::Broke(Broke::Silence { phase: Phase::Data, why }),
(At::Data, Answer::Gone) => Next::Broke(Broke::Gone { phase: Phase::Data }),
(At::DataRestart { unmoved }, Answer::Restarted(true)) => {
- let (trip, act) = Self { moved: self.data_len.saturating_sub(unmoved), ..self }.status(false);
+ let (trip, act) = Self { moved: self.data_len.wrapping_sub(unmoved), ..self }.status(false);
Next::Act(trip, act)
}
(At::DataRestart { .. }, Answer::Restarted(false)) => Next::Broke(Broke::Stall { phase: Phase::Data }),gone-arm-deleted--- a/kernel/src/drivers/xhci/wait/msc.rs
+++ b/kernel/src/drivers/xhci/wait/msc.rs
@@ -754,7 +754,6 @@
fn completed(completion: Result<(u32, u32), Quiet>) -> bot::Answer<Quiet> {
match completion {
Ok((code, residue)) => bot::Answer::Moved { code, residue },
- Err(Quiet::Gone) => bot::Answer::Gone,
Err(why) => bot::Answer::Silent(why),
}
} |
…a break The T14 reading of `usb_transport_break` at 95ebbaf: the boot stick sat on USB2 port 1 at high speed. The staged WRITE(10) break left it owed Data-Out, so the ladder entered at the port rung with no class reset (`ladder::enters_at`). The rung's hot reset completed with the port Enabled (PORTSC 0x00200e03); 50 ms later Address Device answered USB Transaction Error. The rung ended Failed, the climb counted break 2 and entered the Offline rung, which read port 1 Disconnected (0x000202a0), sent nothing and said "is offline". The disk was held, the stick bound on port 13 (a USB 3.1 port, speed 4) as the same device, and the WRITE completed on it. Why the rung could not see it leave: a USB2 port detects no disconnect while it drives a reset (xHCI 1.2 §4.19.1.1.2, note 57) and advances to Enabled when the reset ends (§4.19.1.1.4), and a peripheral uses one connection at a time (§4.19.7). A SuperSpeed stick that trains on the USB3 half therefore leaves the USB2 port after the reset's completion has been read, and the rung's next step meets an empty port. - toyos-xhci: `ladder::holds`, connected with no unconsumed connect change, the predicate `reset_port` already applied before writing a reset. - The climb asks it again when a rung ends unverified. A device its port no longer holds ends the climb as one whose reset read the port empty always did: no break counted, no Offline rung, the device left for the hold a reset of this driver's earns it, and its slot the port's teardown's (§4.4). The Offline rung sent nothing to a port that no longer held its device, so nothing changes on the wire; a class rung whose device left no longer runs the port rung's quiesce. - `usb-reset-moves-after` stages that leave under QEMU: the hold comes after the reset's completion is read with the device on the port. QEMU 11.1.1 answers Address Device on a port with no device with TRB Error (hw/usb/hcd-xhci.c, `xhci_address_slot` and `xhci_lookup_uport`), a failed step as the T14's Transaction Error is. `usb_transport_break` gains `Moved::AfterItsReset`, and no same-stick shape may say the stick is offline. - The metal verdict's kernel half is `usb::transport_break_recovered`: the ladder entered at the port rung, the stick is never offline, and either the port rung took and the WRITE completed, or the stick was held, came back as the same device and the WRITE completed on it, each in order. The "Reset Recovery took" alternative is gone, since this break never enters at the class reset. `metal_usb_judge` holds it to the records the T14 wrote at 95ebbaf (refused) and to the shape this change writes (passed), beside four more. - The SuperSpeed-stick issue gains the reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`cargo run -- --ci host` reddened on clippy::redundant_clone at the two `held.clone()` calls 13ecd39 added to `a_stick_its_reset_moved_carries_on`: each was the value's last use. The needle is a `&str` now, made a `String` at each use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
#616 has the metal judges read the channel their line crosses on. It does not touch `transport_break_on_metal` or its registration, and the merge is clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Mutation patches at dc0863bThe patches behind this round's rows of the negative-control table in the description, exactly as applied. Each is one hunk against h1-holds-ignores-the-connect-changediff --git a/toyos-xhci/src/ladder.rs b/toyos-xhci/src/ladder.rs
index dc9ddc936..f295d0269 100644
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -198,7 +198,7 @@ pub const RESET_RECOVERY_NS: u64 = 50_000_000;
/// no longer holds left: that is no break, no rung above reaches it, and its
/// port's teardown owns what it held (§4.4).
pub fn holds(port: Portsc) -> bool {
- port.connected() && !port.connect_changed()
+ port.connected()
}
/// Whether a failed `step` ends [`Rung::PortReset`].h2-holds-ignores-the-connectiondiff --git a/toyos-xhci/src/ladder.rs b/toyos-xhci/src/ladder.rs
index dc9ddc936..a124096db 100644
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -198,7 +198,7 @@ pub const RESET_RECOVERY_NS: u64 = 50_000_000;
/// no longer holds left: that is no break, no rung above reaches it, and its
/// port's teardown owns what it held (§4.4).
pub fn holds(port: Portsc) -> bool {
- port.connected() && !port.connect_changed()
+ !port.connect_changed()
}
/// Whether a failed `step` ends [`Rung::PortReset`].j0-verdict-as-at-95ebbafdddiff --git a/tests/common/usb.rs b/tests/common/usb.rs
index 1f98e47e7..45f4c1035 100644
--- a/tests/common/usb.rs
+++ b/tests/common/usb.rs
@@ -2156,25 +2156,25 @@ pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String>
"transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ",
)?;
let under_test = broke_on(staged)?;
- let entered = kernel.must_say_after(
- staged,
- &format!("usb-storage: {under_test} is owed the data of the command that broke"),
- )?;
- kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
- kernel.must_not_say(" did not come back within ")?;
- if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {
- let back = kernel.must_say_after(left, " as the same device (USB ")?;
- kernel.must_say_after(
- back,
- "is back, and the operation it was asked went out again on it: it completed",
- )?;
+ if let Ok(left) = kernel.must_say(" after this driver reset it; it is held ") {
eprintln!(" [usb] {left}");
+ let back = kernel.must_say(" as the same device (USB ")?;
eprintln!(" [usb] {back}");
+ kernel.must_say("is back, and the operation it was asked went out again on it: it completed")?;
+ kernel.must_not_say(" did not come back within ")?;
return Ok(());
}
- let took = kernel.must_say_after(entered, &format!("usb-storage: {under_test} the port reset took"))?;
- kernel.must_say_after(took, &format!("usb-storage: {under_test} SCSI 0x2a completed after "))?;
+ let rungs = [
+ format!("usb-storage: {under_test} Reset Recovery took"),
+ format!("usb-storage: {under_test} the port reset took"),
+ ];
+ let took = rungs
+ .iter()
+ .find_map(|rung| kernel.must_say(rung).ok())
+ .ok_or_else(|| format!("neither {:?} nor {:?}: no rung verified", rungs[0], rungs[1]))?;
eprintln!(" [usb] {took}");
+ kernel.must_say(&format!("usb-storage: {under_test} SCSI 0x2a completed after "))?;
+ kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
Ok(())
}
j1-verdict-lets-the-stick-go-offlinediff --git a/tests/common/usb.rs b/tests/common/usb.rs
index 1f98e47e7..dbc583377 100644
--- a/tests/common/usb.rs
+++ b/tests/common/usb.rs
@@ -2160,7 +2160,6 @@ pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String>
staged,
&format!("usb-storage: {under_test} is owed the data of the command that broke"),
)?;
- kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
kernel.must_not_say(" did not come back within ")?;
if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {
let back = kernel.must_say_after(left, " as the same device (USB ")?;j2-verdict-lets-the-class-reset-indiff --git a/tests/common/usb.rs b/tests/common/usb.rs
index 1f98e47e7..4ecc1d192 100644
--- a/tests/common/usb.rs
+++ b/tests/common/usb.rs
@@ -2156,10 +2156,7 @@ pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String>
"transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ",
)?;
let under_test = broke_on(staged)?;
- let entered = kernel.must_say_after(
- staged,
- &format!("usb-storage: {under_test} is owed the data of the command that broke"),
- )?;
+ let entered = staged;
kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
kernel.must_not_say(" did not come back within ")?;
if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {k1-climb-past-a-device-that-leftdiff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs
index 9f29a8bbc..11ab6fbea 100644
--- a/kernel/src/drivers/xhci/wait/msc.rs
+++ b/kernel/src/drivers/xhci/wait/msc.rs
@@ -1134,15 +1134,6 @@ impl XhciController {
Climbed::Failed => None,
};
let ended = Unverified { rung, broke: out_of_step.as_ref() };
- let port = self.read_portsc(dev.port_idx);
- if !ladder::holds(port) {
- log!("usb-storage: {slot} {ended}, and port {} no longer holds the device (PORTSC \
- {:#010x}): its port's teardown takes it from here",
- u32::from(dev.port_idx) + 1, port.raw());
- dev.failed = true;
- dev.left = true;
- return false;
- }
log!("usb-storage: {slot} {ended}; break {} of {MAX_TRANSPORT_BREAKS} running",
dev.run.breaks().saturating_add(1));
// After a step that was not answered the event is spent: the rung |
Review, round 3, at dc0863bCI Earlier BLOCKERs
Growth, from
This round ( BLOCKER
NOTE
REMOVE
SEND BACK |
…eaks the rung's TEST UNIT READY on it Answers the third review of #588 at dc0863b. - BLOCKER 2, one exit for one fact: `Climbed::Gone` is deleted. The port rung's reset step now fails whenever `reset_port` answers anything but `Enumerate`, `AfterReset::Left` included, and the climb's `!holds` read is the one place a rung ends as its device leaving. Every `AfterReset::Left` reads `!holds` there: a port read empty reads connected again only on a new connect, which raises CSC, and the only acknowledge between the two reads is `enumeration_ack`, which clears CSC only after a warm reset whose `after` word carried it. - BLOCKER 1: `usb-reset-moves-configured` holds the port rung once every step was answered and before its TEST UNIT READY, and cues the host to move the stick. QEMU 11.1.1 detaches the slot's port before it updates PORTSC (hw/usb/hcd-xhci.c, `xhci_detach` then `xhci_detach_slot`), and a doorbell on a slot whose port is detached kicks nothing (`xhci_kick_epctx`, `xhci_slot_ok`), so the CBW's wait reads the port disconnected: the rung ends `OutOfStep(Broke::Gone)` on a port that no longer holds the stick. `usb_transport_break` gains `Moved::BeforeItsTestUnitReady`, which asserts `transport broke on the port reset's TEST UNIT READY: the port disconnected during the command phase, and port 1 no longer holds the device` and, with every same-stick shape, no `is offline`. - `holds` moves onto `Portsc`, and the port machine, the HID recovery, the climb and `reset_port` read it; the two site comments that restated it go. - One helper, `hold_for_the_move`, for the three staging holds, and one `reset_moves::take` that takes the actuator it is asked for. - The metal verdict no longer refuses `did not come back within`: no case needed it, since a stick that never came back is refused by its missing same-device line first. - REMOVEs: the chronology in `transport_break_verdict`'s doc, and ", the T14 has not yet" in the offline-disk issue. - The SuperSpeed-stick issue carries the open question of why the port rung's reset moved the stick where the scan's did not. - Filed: a USB3 device attached but untrained reads CAS with CCS clear, and so reads as gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
No conflict. The rust gitlink takes main's c4c65e3e8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Mutation patches at 63a2a04Each is applied with unredlist-transport-break--- a/src/redlist.rs 2026-09-30 16:38:17
+++ b/src/redlist.rs 2026-09-30 16:38:31
@@ -84,10 +84,6 @@
Disabled {
test: "syscall_window_nmi",
issue: "issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md",
- },
- Disabled {
- test: "usb_transport_break",
- issue: "issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md",
},
Disabled {
test: "user_copy_races_munmap",n1-a-broken-test-unit-ready-climbs-on-past-a-device-that-left--- a/kernel/src/drivers/xhci/wait/msc.rs 2026-09-30 16:38:17
+++ b/kernel/src/drivers/xhci/wait/msc.rs 2026-09-30 16:38:31
@@ -1128,7 +1128,7 @@
// disconnect while it drives a reset (xHCI 1.2 §4.19.1.1.2, note
// 57) and reads Enabled once the reset ends (§4.19.1.1.4).
let port = self.read_portsc(dev.port_idx);
- if !port.holds() {
+ if out_of_step.is_none() && !port.holds() {
log!("usb-storage: {slot} {ended}, and port {} no longer holds the device (PORTSC \
{:#010x}): its port's teardown takes it from here",
u32::from(dev.port_idx) + 1, port.raw());
--- a/src/redlist.rs 2026-09-30 16:38:17
+++ b/src/redlist.rs 2026-09-30 16:38:31
@@ -84,10 +84,6 @@
Disabled {
test: "syscall_window_nmi",
issue: "issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md",
- },
- Disabled {
- test: "usb_transport_break",
- issue: "issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md",
},
Disabled {
test: "user_copy_races_munmap",c1-only-the-port-rung-asks-whether-its-device-left--- a/kernel/src/drivers/xhci/wait/msc.rs 2026-09-30 16:38:17
+++ b/kernel/src/drivers/xhci/wait/msc.rs 2026-09-30 16:38:31
@@ -1128,7 +1128,7 @@
// disconnect while it drives a reset (xHCI 1.2 §4.19.1.1.2, note
// 57) and reads Enabled once the reset ends (§4.19.1.1.4).
let port = self.read_portsc(dev.port_idx);
- if !port.holds() {
+ if rung == Rung::PortReset && !port.holds() {
log!("usb-storage: {slot} {ended}, and port {} no longer holds the device (PORTSC \
{:#010x}): its port's teardown takes it from here",
u32::from(dev.port_idx) + 1, port.raw());h1-holds-ignores-the-connect-change--- a/toyos-xhci/src/portsc.rs 2026-09-30 16:38:17
+++ b/toyos-xhci/src/portsc.rs 2026-09-30 16:38:31
@@ -115,7 +115,7 @@
/// replugged between two looks reads connected again, and only CSC says
/// the one that was here has gone.
pub const fn holds(self) -> bool {
- self.connected() && !self.connect_changed()
+ self.connected()
}
/// Whether reset signalling is still on the wire.h2-holds-ignores-the-connection--- a/toyos-xhci/src/portsc.rs 2026-09-30 16:38:17
+++ b/toyos-xhci/src/portsc.rs 2026-09-30 16:38:31
@@ -115,7 +115,7 @@
/// replugged between two looks reads connected again, and only CSC says
/// the one that was here has gone.
pub const fn holds(self) -> bool {
- self.connected() && !self.connect_changed()
+ !self.connect_changed()
}
/// Whether reset signalling is still on the wire.j0-verdict-as-at-95ebbafdd--- a/tests/common/usb.rs 2026-09-30 16:38:17
+++ b/tests/common/usb.rs 2026-09-30 16:38:45
@@ -2192,24 +2192,25 @@
"transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ",
)?;
let under_test = broke_on(staged)?;
- let entered = kernel.must_say_after(
- staged,
- &format!("usb-storage: {under_test} is owed the data of the command that broke"),
- )?;
- kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
- if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {
- let back = kernel.must_say_after(left, " as the same device (USB ")?;
- kernel.must_say_after(
- back,
- "is back, and the operation it was asked went out again on it: it completed",
- )?;
+ if let Ok(left) = kernel.must_say(" after this driver reset it; it is held ") {
eprintln!(" [usb] {left}");
+ let back = kernel.must_say(" as the same device (USB ")?;
eprintln!(" [usb] {back}");
+ kernel.must_say("is back, and the operation it was asked went out again on it: it completed")?;
+ kernel.must_not_say(" did not come back within ")?;
return Ok(());
}
- let took = kernel.must_say_after(entered, &format!("usb-storage: {under_test} the port reset took"))?;
- kernel.must_say_after(took, &format!("usb-storage: {under_test} SCSI 0x2a completed after "))?;
+ let rungs = [
+ format!("usb-storage: {under_test} Reset Recovery took"),
+ format!("usb-storage: {under_test} the port reset took"),
+ ];
+ let took = rungs
+ .iter()
+ .find_map(|rung| kernel.must_say(rung).ok())
+ .ok_or_else(|| format!("neither {:?} nor {:?}: no rung verified", rungs[0], rungs[1]))?;
eprintln!(" [usb] {took}");
+ kernel.must_say(&format!("usb-storage: {under_test} SCSI 0x2a completed after "))?;
+ kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
Ok(())
}
j1-verdict-lets-the-stick-go-offline--- a/tests/common/usb.rs 2026-09-30 16:38:17
+++ b/tests/common/usb.rs 2026-09-30 16:38:31
@@ -2196,7 +2196,6 @@
staged,
&format!("usb-storage: {under_test} is owed the data of the command that broke"),
)?;
- kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {
let back = kernel.must_say_after(left, " as the same device (USB ")?;
kernel.must_say_after(j2-verdict-lets-the-class-reset-in--- a/tests/common/usb.rs 2026-09-30 16:38:17
+++ b/tests/common/usb.rs 2026-09-30 16:38:31
@@ -2192,10 +2192,7 @@
"transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ",
)?;
let under_test = broke_on(staged)?;
- let entered = kernel.must_say_after(
- staged,
- &format!("usb-storage: {under_test} is owed the data of the command that broke"),
- )?;
+ let entered = staged;
kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?;
if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") {
let back = kernel.must_say_after(left, " as the same device (USB ")?; |
|
Orchestrator runs at 63a2a04 (
CI |
Review at 63a2a04 (round 5)CI
The merges fef8cfd and 63a2a04 resolved nothing ( Earlier BLOCKERs (the review at dc0863b)
GrowthFrom
This round ( BLOCKER
NOTE
REMOVE
SEND BACK |
tests/checks.rs conflicted: #630 adds the metal_checks tests beside metal_usb_judge and deletes the doc line naming tests/metal-profile.toml. Both sides' hunks are kept: metal_usb_judge, then main's metal_checks tests, and the doc line goes with main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
… fix's test Review round 5 of #588: the fix's only test was usb_transport_break, which is redlisted and which #639 deletes. Whichever landed first, main kept the fix with nothing that could go red on it. toyos_xhci::ladder now decides what a rung that did not verify leaves: - `Run::unverified(&Unverified<W>, holds)` answers `AfterRung::Left` for any rung, however it ended, on a port that no longer holds its device, and otherwise the next climb with the transfer event the next rung quiesces against. The kernel reads PORTSC once the rung has ended and applies the answer; its `Climbed` enum is replaced by `Result<(), Unverified>`. - `AfterReset::goes_on` is the port rung's decision to go on past its reset. - `a_rung_that_did_not_verify_left_exactly_where_its_port_no_longer_holds_the_device` covers both rungs, every ending, and both port readings, so the review's n1 and c1 narrowings, the arm deleted, and B2's fold of `Left` into a reset that took are crate mutations `cargo test -p toyos-xhci` turns red. usb_stick_left (tests/common/usb.rs) stages the leave on the gate's data stick, which the host unplugs on the hold's cue and never plugs back: the three holds `usb-reset-moves`, `-after` and `-configured`. Each boot judges the leave line, no `is offline`, no `break 2`, and the teardown's `slot N disabled` after the leave; the `usb-reset-moves` and `-configured` boots refuse `Reset Device failed` and `Address Device (after the port reset)` between the hold and the leave. It has no redlist row. The T14 row judged by `transport_break_on_metal` moves onto it; its comment is deleted, since the verdict refuses the class reset it named. usb_transport_break and its redlist row are as on main again: the `AfterItsReset` and `BeforeItsTestUnitReady` boots and the `is offline` refusal leave it, and the actuators only it arms are untouched. Filed: a bind whose device left climbs the ladder at its empty port, the review's two-exits NOTE, which main has too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Each issue whose test this branch deleted names the commit that took out what only that test used, beside the test's own, as the revert that brings both back: `quiesce-last-teardown` (b82f45e), `console-queue-at-the-stop` (2718404), `copy-meets-a-remap` (2f366a9), the ten USB actuators (b5c59cb, and 9ebf080 for `usb-transport-break` and `usb-reset-break`), `SYS_DEBUG` 17 and 18 (009db6d), and the IST-off control with the storm's hold (5399770). Where a restore needs a later commit reverted first, the revert names it and says what else comes back. The `usb_transport_break` issue goes back to `status: open`: #588 keeps that test red and does not hold it. Its text now holds whichever of #588 and #639 lands second, and says what that lander does: `tests/common/usb.rs` resolved as the deletion with every hunk of the other side accounted for, #588's two actuators deleted with the rest, and the commit holding #588's version of the test recorded there. Each filed flake deleted this round has its issue name the commit: its own issue where it has one, `parallel-tests-red-under-other-suites` and `a-loaded-suite-reds-a-volume-checker-on-both-arms` otherwise, the first of them also saying which of its names were not deleted and why. The single-test flake issues become `kind: defect`, since a deleted test blocks no landing; `usb_short_read`'s and `partition_claim_gives_up`'s gain an exit. Issues that named a deleted test as their gate get one line pointing at where its restore is recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Mutation patches at 989aec4Each applies with
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -150,7 +150,7 @@
/// the device: its port's teardown owns what it held. Otherwise the rung
/// is the next break.
pub fn unverified<W>(&mut self, ended: &Unverified<W>, holds: bool) -> AfterRung {
- if !holds {
+ if !holds && matches!(ended, Unverified::Failed) {
return AfterRung::Left;
}
// After a step that was not answered the event is spent: the rung has
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -150,7 +150,7 @@
/// the device: its port's teardown owns what it held. Otherwise the rung
/// is the next break.
pub fn unverified<W>(&mut self, ended: &Unverified<W>, holds: bool) -> AfterRung {
- if !holds {
+ if !holds && self.climbed == Some(Rung::PortReset) {
return AfterRung::Left;
}
// After a step that was not answered the event is spent: the rung has
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -149,10 +149,7 @@
/// however the rung ended.** That is no break, and no rung above reaches
/// the device: its port's teardown owns what it held. Otherwise the rung
/// is the next break.
- pub fn unverified<W>(&mut self, ended: &Unverified<W>, holds: bool) -> AfterRung {
- if !holds {
- return AfterRung::Left;
- }
+ pub fn unverified<W>(&mut self, ended: &Unverified<W>, _holds: bool) -> AfterRung {
// After a step that was not answered the event is spent: the rung has
// commanded the pair since, and only the fields speak for it now.
let broke = match ended {
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -262,7 +262,7 @@
/// was enumerated as, on its port. Any other reading ends the rung, and
/// nothing is sent to a port that reads empty.
pub fn goes_on(self) -> bool {
- self == Self::Enumerate
+ matches!(self, Self::Enumerate | Self::Left)
}
}
--- a/kernel/src/drivers/xhci/wait/msc.rs
+++ b/kernel/src/drivers/xhci/wait/msc.rs
@@ -1275,7 +1275,7 @@
self.quiesce_bulk_pair(dev, broke, "stopping it before its port is reset")
}
// `reset_port` has said which way it did not.
- PortStep::Reset => self.reset_port(dev, RECOVERING).goes_on(),
+ PortStep::Reset => matches!(self.reset_port(dev, RECOVERING), AfterReset::Enumerate | AfterReset::Left),
PortStep::Settle => {
let _ = crate::clock::settles(
self.after_break
--- a/toyos-xhci/src/ladder.rs
+++ b/toyos-xhci/src/ladder.rs
@@ -138,7 +138,9 @@
/// A round trip completed: the run is over. How many breaks it held.
pub fn over(&mut self) -> u8 {
- core::mem::replace(self, Self::NONE).breaks
+ let breaks = self.breaks;
+ self.breaks = 0;
+ breaks
}
/// The rung this run last climbed ended `ended`, below [`Rung::Offline`], |
|
Orchestrator runs at 989aec4 (logs
The T14 run also recorded three new rows for the |
Review at 989aec4 (round 6, final)CI The orchestrator's runs at 989aec4 are in issuecomment-5919300334 (
Crate controls, from the PR body: n1, c1, k1 and b2c each give Merges: Earlier BLOCKERs (round 5, at 63a2a04)
GrowthMeasured with
Accepted. This round's 32 net production lines move the leave decision into the crate, where a host test can turn red on it. BLOCKERNone open. NOTE
REMOVE
LAND AFTER NAMED CHANGES |
The `--metal usb_stick_left` run at 989aec4 passed and recorded three new numbers for the `usbbreak` boot: complete_ms 1160, panel_max_us 3905, panel_us 21851. On `main` the redlist keeps that boot from running; this branch is what runs it, so its records land here. The same run's `boot.testcases` rows are not this branch's and are left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…toyos-noredlist #588 landed first, and its `usb_stick_left` arms `usb-transport-break`, `usb-reset-moves`, `usb-reset-moves-after` and `usb-reset-moves-configured` again, its T14 row (`usbbreak`) arms `usb-transport-break`, and its metal record carries the `usbbreak` rows. Those stay, with `msc::transport_break`, `msc::reset_moves`, `RECOVERING` and `Quiet::Staged`. Everything else this branch deleted with `usb_transport_break` (90ecefe, b5c59cb, 402107b, 9ebf080) stays deleted: no test on either side arms it. Conflicts, every hunk: - `kernel/src/actuator.rs`, two hunks. First: main's eight USB rows. Kept `usb_transport_break`, `usb_reset_moves`, `usb_reset_moves_after` and `usb_reset_moves_configured`, with `usb_reset_moves`' doc losing its citation of the deleted `usb_transport_break`; dropped `usb_transport_offline`, `usb_reset_break`, `usb_transport_break_owed` and `usb_transport_break_flushed`, armed only by the deleted `usb_transport_break`, `log_flush_retry` and `partition_claim_departure`. Second: dropped `watch_window` (`blocking_read_window`, deleted in dc62efc), kept #634's `handler_post`. - `kernel/src/drivers/xhci/wait/msc.rs`, fourteen hunks, misaligned by #588's rewrite of the file. Resolved as main's file with this branch's deletions applied to it: `Asks` and `bot`'s `asks` argument at its five call sites; `transport_break`'s owed and flushed half (`WROTE`, `FLUSHED`, `AFTER_A_FLUSH`, `wrote`, `flushed`, their calls in `MscDevice::wrote` and the flush, and `arm`'s argument), so `arm` reads `usb-transport-break` alone; `return_silent` and its hook in `served`; `reset_break` and the wrapper around `climb`, whose body `climb_until_in_step` carries again; `short_read` and its hold and release around the data phase; `staged` and its checks in `bot`, the bad signature, the withheld CBW, the gone port and the unanswered wait; the bind's `slow_return` stall and INQUIRY staging, and `slow_return` itself; `read_serial`'s short ask. `reset_moves`, its three holds and `RECOVERING` are main's as they stand. - `kernel/src/watch.rs`, one hunk: dropped the `window` module (`watch-window`), kept #634's `handler_post` module. - `toyos-sched/src/watch.rs`, one hunk: the same, `window` dropped and `handler_post` kept. - `src/lib.rs`, one hunk: kept #652's `pub mod n2`, dropped `pub mod redlist`. - `tests/common/power.rs`, one hunk: main's only change there renamed `transport_break_chain`'s doc to `usb_stick_left`; its one caller is the deleted `usb_transport_break`, so it stays deleted. - `tests/common/usb.rs`, one hunk holding main's whole `usb_transport_break` region. Kept #588's `Held`, `usb_stick_left`, `a_stick_that_left_under_its_rung`, `transport_break_on_metal`, `transport_break_recovered` (which `tests/checks/usb.rs` judges) and `broke_on`; the rest is `usb_transport_break`'s and stays deleted: `a_read_whose_first_wait_spent_its_budget_goes_out_again`, `serial_short_is_not_read`, `cpu_of`, `line_with`, `Moved`, `a_stick_its_reset_moved_carries_on`, `abandoned_write_is_taken_offline`, `no_command_was_refused`, `port_gone_is_left_to_the_teardown`, `control_requests`, `command_blocks`, `every_reset_is_followed_by_a_test_unit_ready`, `is_a_rungs_configuration`, `every_port_reset_is_followed_by_a_test_unit_ready`, `every_reset_is_followed_by_both_clears` and `transport_gives_up`. - `tests/toyos.rs`, five hunks. `MACHINE_TESTS`: kept `usb_stick_left` and #634's `handler_post_without_a_pass`; dropped `usb_transport_break`, `xhci_full_speed_device` (5e42235), `blocking_read_window` and `user_copy_races_munmap` (7ea6be1). `METAL`: kept #588's `usb_stick_left` row. Dispatch: kept `usb_stick_left` and `handler_post_without_a_pass`; dropped `usb_transport_break`, `xhci_full_speed_device` and `smp_failed_ap_leaves_no_hole` (aedcf17). Outside the conflicts: - `kernel/src/drivers/xhci/wait/mod.rs`: `Quiet::Staged` and its line come back, which `transport_break::take` answers with; the auto-merge had taken 9ebf080's deletion. The `reset_break` and `return_silent` hooks stay deleted. - `src/metal.rs`: `FLASHABLE`'s `usb-transport-break` row comes back (b5c59cb took it), since `usb_stick_left`'s T14 arm flashes it. - `issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md`: records `4f2bea143`, this merge's second parent, as the tree holding #588's version of `usb_transport_break`, in place of the instruction to record it. - `issues/boot-media/logd-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md`: drops its citation of `power::transport_break_chain`, deleted in 90ecefe. The `rust` gitlink is main's. On this tree: `cargo run -- --build-only` exit 0; the kernel's `cargo check` for x86-64 and AArch64, bare, with `boot-actuators` and with `boot-actuators,test-actuators`, exit 0 each; `cargo test --test toyos-build --no-run` exit 0, no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Main is 469 commits past the last merge (e754dc8, #588). Six files conflicted, each resolved by accounting for both sides: - kernel/src/actuator.rs: main (#639, #660) deleted every actuator in the region this branch added `windows-staged` to, `handler-post` among them. Main's deletions stand, and `windows-staged` sits after `usb-reset-under-load`. Its doc names `virt_mask_windows`, its one judge now. - kernel/src/scheduler.rs: main deleted `dump-in-blocking-pass`'s lines in `exit_to_user`; this branch's `irqs_unmasking` hook followed them. The hook stays, the actuator's lines go. - src/clippy.rs: main dropped `fpu-save-nothing` and `user-writable-gsbase` from both instrument lists; `mask-windows` is appended to what is left, and the two `MASK_WINDOWS_KERNEL` shapes are this branch's. - src/build.rs: main cut `TEST_SUITE_KERNEL_BUILDS` to the shipping and the test kernel; `MASK_WINDOWS_KERNEL` is the third. `carries_the_actuators` and `refuse_an_unarmable_boot` merged without conflict. - tests/common/qemu.rs: main shortened `DECLARED_KERNEL_BUILDS`' doc and deleted `kernel_of`'s paragraph on `boot_image`; this branch's sentence for the windows' build and `kernel_of`'s rule are kept over main's text. - tests/toyos.rs: main's file (#660: 21 declared guest tests, `Sched` and the shared guest boot gone, `metal::Metal` a struct) taken whole, and this branch's additions put back onto it: `ring_park_herd` on `RUST_SKIP`, `virt_mask_windows` and its judge `mask_windows`, the `mask_windows` metal row with `WINDOWSCASE`, and `windows_on_metal`. **The x86-64 guest test `mask_windows` does not come back.** Main refuses one name in two registries (`registered`), and `mask_windows` is the T14's row; `irq_census_conservation`, the census these lines sit beside, was cut from the guest suite for its row the same way. Its `MACHINE_TESTS` entry, its `CARRIES` row and its `run_machine_test` arm, which booted the shared guest image main deleted, are not carried. What it held is held by: the x86-64 hooks' checks by the metal row, which boots them under the herd; the stage by `virt_mask_windows`; the verdict by `mask_windows_verdict` on the host. The census of raw masking instructions is unchanged by the merge: every word of `cli`, `sti`, `popfq`, `iretq`, `sysretq`, `eret`, `daifset`, `daifclr`, `daif`, `wfi` and `hlt` under `kernel/src`, comments apart, is the same list at 737803d and here but for main's own two moves: the NMI gate's `iretq` from `nmi_gate.rs` to `idt/nmi.rs`, and `IrqGuard::unclosed`, a read of the flags, deleted on both architectures. The clause of the guard's drop hook that named it goes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
The USB mass-storage BOT round trip and the SCSI bring-up above it become pure
toyos-xhcicode with host tests.kernel/src/drivers/xhci/wait/msc.rskeeps the shim: it queues the transfers the machines ask for, waits for them in place, publishes the phase to the reset path, runs the staged actuators, and writes the log lines. It still runs the ladder's climb loop and decides that a disconnect mid-command goes to the port's teardown,offline: dev.failed, anddata_out. A userland usbd can drive the same machines unchanged. The crate isno_std, has no unsafe code and makes no x86 assumptions.The T14 reading of this branch showed one defect in the ladder that the branch carried over from
main: a stick that left its port under the port rung's reset was counted as a break and narrated as taken offline. That is fixed here too; see the ladder section.What changed and why
toyos_xhci::botcbw()builds the 31-byte CBW (USB MSC BOT 1.0 §5.1): LUN 0, direction taken from the CDB.RoundTripis the round trip as a stepped machine. Its acts areCommand,Data(pipe),StatusandRestart { pipe, then }, and it is answered withMoved { code, residue },Silent,GoneorRestarted.CswDue::judgechecks signature and tag (§6.3.1), then applies §6.3.2's meaningful test. Status 2 is a phase error whatever the residue. Statuses 0 and 1 are refused when the residue is larger than the transfer. Statuses 3..=255 areBroke::Reserved. Delivered bytes aremin(controller's count, device's count), and a controller residue larger than the transfer delivers nothing.Broke<W>carriesleft()andevent()and is generic over the driver's silence reason.Gonevariant; it used to beSilence { why: Quiet::Gone }. The crate has to tell a disconnect apart, and the kernel'sQuietis not a type the crate has.Broke<Quiet>::Silence { why: Quiet::Gone }can still be built, and onlycompleted()inmsc.rskeeps it apart fromBroke::Gone. It is not made unrepresentable because the price is a second kernel enum that mirrors three ofQuiet's four variants (one of themcfg'd), with conversions both ways, all to guard one match in one function. Deleting that match'sGonearm is the negative control below, run in the guest.left()matches on aPhase. It used to match on a phase's name string.CC_STALLjoinsCC_SUCCESSandCC_SHORT_PACKETintoyos_xhci::job. The kernel imports all three from there, and its own copies are deleted.toyos_xhci::scsiCdbcan only be built inside this module, and it carries its own direction. The kernel'scdb_len/data_inparameters and its CDB-shape assert are gone, because a mismatch can no longer be written.Sense::ofbelieves sense data only if at least 14 bytes arrived (ASCQ is byte 13; SPC-4 §4.5.3).Replyis the answer to one SCSI command. It is named apart from the crate root'stoyos_xhci::Outcome.flushed(): INVALID COMMAND OPERATION CODE means the device has no cache, which is not a failure.Transferdoes the READ/WRITE(10) batching and sector addressing. Only the first batch may answer "ask again".Geometry's fields are private and read through accessors, so only the bring-up can size a disk, and theexpectinTransfer::nextdepends on that.Geometry::NONEfits no transfer, and a test checks it.BringUpis a stepped machine:Refusal).BringUpblocking, the same way the boot scan drives enumeration. A stepped kernel driver for the hot-plug bind is not built here; the renamed issue below owns that.Printablemoved here unchanged.toyos_xhci::ladder::Runholds a device's break count and highest rung. It used to be two loose fields.identity::first_languagereads the LANGID of string descriptor zero.The ladder: a rung whose device left its port ends as the device leaving.
usb_transport_breakon metal, SanDisk 0781:5581 on the xHC at 00:14.0):ladder::enters_at, unchanged; the log says so by name). This is the laddermainhas:enters_at,next, the port rung's steps andtake_offlineare the same there.PORTSC 0x00000e03 then 0x00200e03). 50 ms later Address Device answered USB Transaction Error.Failed, the climb counted break 2 and entered the Offline rung. That rung read port 1 Disconnected (0x000202a0: CCS clear, CSC set, RxDetect), wrote no reset and sent no command, and saidis offline.Portsc::holds: connected, with no unconsumed connect change. The port machine, the HID recovery, the climb andreset_portall read it; the first two spelled it inline before.toyos_xhci::ladder's.Run::unverified(&Unverified<W>, holds)answersAfterRung::Leftfor a rung below Offline, whichever rung and however it ended, on a port that no longer holds its device: no break is counted and no rung above runs. Otherwise it answers the next climb and the transfer event the next rung quiesces against. The kernel reads PORTSC once the rung has ended, applies the answer, and writes the line:the port reset was not answered, and port 1 no longer holds the device (PORTSC 0x000202a0): its port's teardown takes it from here. The disk is held if a reset of this driver's earns it one, and the slot is the port's teardown's (§4.4). The kernel'sClimbedenum is replaced byResult<(), ladder::Unverified<Quiet>>.AfterReset::goes_onis the port rung's decision to go on past its reset: onlyEnumerate. A reset that reads the port empty (AfterReset::Left) ends the rungFailedand reaches the same read. EveryAfterReset::Leftreads!holdsthere: an empty port reads connected again only on a new connect, which raises CSC, and the only acknowledge between the two reads isenumeration_ack, which clears CSC only after a warm reset whose read carried it.c1below is its host control, and its outcome ismain's: the device is left to the teardown.Gonestill climbs, and meets the empty port only at the first rung's end.mainhas that too: filed asissues/kernel/a-bind-whose-device-left-aims-the-ladder-at-its-empty-port.md.usb_stick_left, the fix's test, with no redlist row (tests/common/usb.rs):Profile::UsbDisk) takes the staged break on its first WRITE(10), is owed its data, and the ladder enters at the port rung.reset_movesholds the rung once; the host unplugs the stick on the hold's cue (device_delofusbdev0) and never plugs it back, so no call waits on a device that returns.usb-reset-moves, before the reset's completion is read: the reset reads the port empty.usb-reset-moves-after, once the completion has been read with the stick on the port. QEMU 11.1.1 answers Address Device for a port with no device with TRB Error (hw/usb/hcd-xhci.c,xhci_address_slotthroughxhci_lookup_uport), so the rung fails at the step the T14's did.usb-reset-moves-configured, once every step of the rung was answered and before its TEST UNIT READY. QEMU detaches the slot's port before it updates PORTSC (xhci_detachcallsxhci_detach_slotfirst), and a doorbell on a slot whose port is detached kicks nothing (xhci_kick_epctx,xhci_slot_ok), so the CBW's wait reads the port disconnected and the rung endsOutOfStep(Broke::Gone).xHCI: slot N disabledfor the stick's slot. It refusesusb-storage: <slot> is offlineand anybreak 2 ofline of that slot.usb-reset-movesand-configuredboots refuseReset Device failedandAddress Device (after the port reset)between the hold and the leave, which B2's fold would send to the empty port.transport_break_on_metalmoves fromusb_transport_breakontousb_stick_left; its arm (usbbreak,tests/jobcase,usb-transport-break) is unchanged.usb_transport_breakand its redlist row are as onmain. This branch'sAfterItsResetandBeforeItsTestUnitReadyboots and itsis offlinerefusal leave it, and every actuator only it arms is untouched for #639. It sharesusb-reset-movesand the metal verdict'stransport_break_on_metalwithusb_stick_left.The metal verdict (
tests/common/usb.rs). Its kernel-log half istransport_break_recovered:is offline.Reset Recovery tookalternative is gone, since this break never enters at the class reset. The old moved branch did not refuseis offline, and it passed the reading above.metal_usb_judge(tests/checks/usb.rs) holds it to the T14's records at 95ebbaf (refused) and to the shape this branch writes (passed), beside five more.Kernel shim:
msc.rsgoes from 2642 to 2372 lines, andkernel/srcfrom 62668 to 62400, onmainat 3c24b6e and on this head. The stagedPortGonefault is now the Command act's completion,completed(Err(Quiet::Gone)), so the actuator goes through the arm that sends a disconnect to the teardown. Nothing is queued for it, as before.Issues
the-bot-scsi-machine-is-still-hand-written-in-the-kernelis no longer true. What the issue still owes is the bind, which is the one call site inside a scheduling pass. So it is renamed toissues/hardware/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md, and its citation moves with it (issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md).issues/hardware/the-t14s-superspeed-stick-enumerates-at-high-speed-under-toyos.mdgains the reading above, and the open question of why the port rung's reset moved the stick where the scan's reset of the same port did not.issues/kernel/a-usb3-device-attached-but-untrained-reads-as-gone.md. A USB3 port that detected a device it could not enable reads CAS with CCS clear, so it reads!holds, and nothing here warm-resets it. That holds onmaintoo.issues/kernel/a-bind-whose-device-left-aims-the-ladder-at-its-empty-port.md, above.Behaviour
The refactor changes nothing on the wire: the commands, bytes, order, waits and recovery are the same. The two source spellings that
toyos-blackboxchecks againstmsc.rsare kept. The ladder fix above is the one behaviour change, and it sends nothing new.These things changed off the wire:
Unaligned: the same bytes at the same DMA offset. The CSW is read as one 13-byte copy.StatusOwedtwice, with the same value and ring positions.Transfer::nextnow panics (expect) where the old code silently truncated a sector number to 32 bits. Bring-up's refusal of disks past 2^32 sectors and the range check already make that case unreachable.msc_flush's innerif !dev.no_write_cacheis removed. It was always true, because the latch is checked above it.CSW claims N B unmoved, and a reserved status (3..=255) logsthe CSW carries status 0x.., which the class reservesinstead of a phase error. Both break in the status phase and climb the same ladder as before.boot-actuators, the stagedPortGonenow takes a tag and writes its CBW into the pool before its Command act completes as the disconnect. Nothing reaches the controller.Gates
At 989aec4 (logs under the job scratchpad,
orch/588r6/logs/):cargo run -- --ci host([ci] Host: 55 step(s), all green)ci-host.txtcargo run -- --build-onlybuild-only.txtcargo test -p toyos-xhci(150 tests)green-xhci.txtcargo clippy -- -D warningswithsrc/clippy.rs's adopted lints, x86_64 and AArch64, each with no features,boot-actuators, andboot-actuators,test-actuatorsgreen-clippy-clippy-{1..6}.txtcargo test --test toyos-checks metal_usb_judgegreen-checks.txtcargo test --test toyos-build --no-rungreen-harness.txtAt d1471df, which adds the T14's three
boot.usbbreak.*records (1160 / 3905 / 21851) from the 989aec4 run below totests/metal/lenovo-20w0003amz.toml:cargo run -- --ci host([ci] Host: 55 step(s), all green) exit 0, logorch/588r7/ci-host.txt.The orchestrator's runs at 989aec4 (
orch/logs/588r6-588r6-*.log, issuecomment-5919300334):usb_stick_left, run 1cargo test --test toyos-build -- usb_stick_left588r6-588r6-stick-left-1.logusb_stick_left, run 2cargo test --test toyos-build -- usb_stick_left588r6-588r6-stick-left-2.logusb_stick_left, run 3cargo test --test toyos-build -- usb_stick_left588r6-588r6-stick-left-3.logcargo test --test toyos-build -- usb_stick_leftBeforeItsCompletion: "[kernel 0.324 cpu0] xHCI: Address Device (after the port reset) failed: code 5 (TRB Error)" between the hold and the leave588r6-588r6-b2k-kernel-fold.logcargo test --test toyos-build499 passed, 499 total588r6-588r6-whole.logcargo test --test toyos-build -- --metal usb_stick_leftPASS usb_stick_left588r6-588r6-metal.logThe orchestrator's runs at 63a2a04 (
orch/logs/588r5-*.log, issuecomment-5914619271), before the leave decision moved intotoyos_xhci::ladder:cargo test --test toyos-build -- --metal usb_transport_breakthe port reset was not answered, and port 1 no longer holds the device (PORTSC 0x000202a0): its port's teardown takes it from here, thenPASS usb_transport_break588r5-metal-forced.logHigh-risk: negative controls and the independent oracle
Negative controls. Each is a checked patch:
git apply --check, applied, shown to build, its test run,git apply -R, and the tree clean after. The patches are in the comment "Mutation patches at 989aec4".The green arm is the gates above at 989aec4.
Run::unverifiedleaves only aFailedrung:if !holds && matches!(ended, Unverified::Failed)cargo test -p toyos-xhci --no-run0cargo test -p toyos-xhciEXIT=101,a_rung_that_did_not_verify_left_exactly_where_its_port_no_longer_holds_the_device:ClassReset OutOfStep(Code { .. })answeredClimbs, notLeftif !holds && self.climbed == Some(Rung::PortReset)ClassReset FailedansweredClimbs!holdsarm deletedClassReset FailedansweredClimbsAfterReset::goes_onfoldsLeftintoEnumerate, B2's fold in the cratea_device_is_addressed_again_only_as_what_it_was_enumerated_as:LeftPortStep::Reset => matches!(self.reset_port(dev, RECOVERING), AfterReset::Enumerate | AfterReset::Left)cargo test --test toyos-build --no-run0usb_stick_leftEXIT=1, the Gates row "b2k applied"Earlier rounds' controls, re-run at 989aec4, each built (EXIT=0) and red: m01 to m23 (
cargo test -p toyos-xhciEXIT=101; m16 re-expressed at this head, sinceRun::unverifiednow followsRun::over), h1 and h2 (EXIT=101,a_port_holds_its_device_only_while_it_reads_connected_with_no_change_unconsumed), j0, j1 and j2 (cargo test --test toyos-checks metal_usb_judgeEXIT=101). Logolder-controls.txt.a_cbw_is_laid_out_as_the_class_defines_ita_csw_is_believed_only_when_it_is_valid_and_meaningfulwhat_is_delivered_is_what_both_the_controller_and_the_device_say_arrived,a_stalled_data_phase_delivers_what_both_the_controller_and_the_device_say_arriveda_stalled_status_is_asked_for_again_oncea_csw_is_believed_only_when_it_is_valid_and_meaningful, by the subtraction underflow the check exists to preventa_command_or_status_block_that_moved_short_breaksbottestssense_is_believed_only_when_its_ascq_arrivedonly_the_first_batch_may_answer_ask_againa_disk_too_big_for_read_capacity_10_is_asked_the_16_byte_forma_flush_the_device_does_not_implement_is_no_failure_and_every_other_refusal_isa_disk_that_never_becomes_ready_is_given_up_on_at_its_budgeta_transfer_is_its_batches_in_order_and_only_a_whole_one_advancesa_device_that_is_not_a_disk_this_driver_serves_is_refused_by_nameRun::overkeeps its runga_run_climbs_one_rung_per_break_until_a_round_trip_ends_itladdertestsa_language_is_read_only_from_a_string_descriptor_that_carries_onemoved: self.data_len(the device's residue alone)a_stalled_data_phase_delivers_what_both_the_controller_and_the_device_say_arrived.status(true)(no §5.3.3 retry)a_status_stalled_after_a_data_stall_is_still_asked_for_againwrapping_subwhat_is_delivered_is_what_both_the_controller_and_the_device_say_arriveda_csw_is_believed_only_when_it_is_valid_and_meaningfulwrapping_suba_stalled_data_phase_delivers_what_both_the_controller_and_the_device_say_arrivedThe disconnect seam's control is
gone-arm-deleted(the comment "Mutation patches at 95ebbaf"). It deletesErr(Quiet::Gone) => bot::Answer::Gone,fromcompleted(), and it still applies here (git apply --check0). At 95ebbaf,cargo test --test toyos-build -- usb_transport_breakwith it applied exited 1:the driver never said "… transport broke on SCSI 0x28: the port disconnected during the command phase; its port's teardown takes it from here"(log588r3-control-gone-arm.log).Independent oracles.
metal_usb_judgecarries them, and the T14 at 63a2a04 passed. The xHCI 1.2 sections above were read from the specification's text. The QEMU stagings' completions and detach order were read from QEMU 11.1.1'shw/usb/hcd-xhci.c, the instrument.github/qemu-versiondeclares.usb-storagedevice is a second implementation of the same protocol, reached through the guest USB suite. The T14's stick is a third.What I am unsure of
usb_stick_left, whose QEMU boots stage the data stick. The QEMU replay of the T14's own boot,power::transport_break_chain, isusb_transport_break's and goes with it in The redlist and the recorded durations go: a red test is fixed or deleted with its issue, a flaky test is deleted at once, and shards deal the task list in turn #639.🤖 Generated with Claude Code
https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L