Artemis Milestone 2g (partial): bulk endpoint discovery + 2e disconnect teardown

Picked up from a crashed session: xhci_driver.h/xhci.h already had the
bulk_in/out_ep_addr/max_packet fields and Endpoint-descriptor offset
macros scaffolded, but the actual walk that populates them was never
written. Added it: after 2f confirms a Mass Storage/SCSI/BOT interface,
a nested walk continues through the Endpoint descriptors that follow it
(bDescriptorType==5, stopping at the next Interface descriptor or end
of stream), keeping only Bulk-type endpoints and splitting IN/OUT by
bEndpointAddress bit 7. Also reset the four new fields in
xhci_bringup(), which the scaffolding had missed.

Also completed 2e's disconnect teardown, which was fully implemented
this session (not scaffolded): a Disable Slot command is now submitted
on a real disconnect, with the port's tracked slot ID captured and
cleared from port_slot_id[] immediately (before the command completes)
so a fresh connect on the same port isn't confused for one already in
progress, and DCBAA[slot_id] cleared only on a successful completion.

Verified live via QMP hotplug (deliberate device_add/device_del against
freshly launched, individually-tracked instances -- not whatever
happened to be attached at boot), all three architectures,
byte-identical: bulk IN endpoint=0x81, bulk OUT endpoint=0x02, then a
clean disconnect -> disable slot succeeded, no wedge. Caught and fixed
a documentation near-miss in the same pass: an initial draft cited the
probe-free three-arch acceptance boots as this feature's verification
evidence, but a stale leftover log directory from a pre-crash orphaned
QEMU process had been picked up by an `ls -dt | head -1` glob during
monitoring and mistaken for this session's own result -- the real
acceptance logs never had a device attached at all. Re-verified against
real PIDs and real log paths before writing FABRIC-2.md's final
writeup.

FABRIC-2.md Section X Milestone 2 updated: 2e's disconnect-teardown
checklist item marked done, 2g's endpoint-identification item marked
partially done (identification only -- Configure Endpoint / EP Context
wiring is still open).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4VMX6VSKCten8nGgaMkq4
This commit is contained in:
Robert Allan James
2026-08-25 08:17:50 -04:00
co-authored by Claude Sonnet 5
parent b4bbd043d0
commit 96d55fcd87
15 changed files with 54535 additions and 13 deletions
+74 -6
View File
@@ -3211,9 +3211,10 @@ Final three-arch acceptance (probe-free, `clean qemu`): all three boot to `ok>`
the device (Address Device command) — **both done 2026-08-22**, see writeup below.
Device descriptor read is 2f's job and depends on this (not the reverse — an earlier
version of this note had the dependency backwards)
- [ ] On disconnect: tear down the corresponding device slot and signal to whatever higher-
level code (Section U's identity/VM logic) that the device is gone — **this is the
first point where Section U/V's design actually gets a real trigger to hang off**
- [x] On disconnect: tear down the corresponding device slot **done 2026-08-25**, see
writeup below. Signaling to Section U's higher-level code is still open — a real
callback surface doesn't exist yet, this item only covers the xHCI-side teardown
(Disable Slot command, DCBAA entry clear)
- [ ] Decide and implement where the hotplug event surfaces to the rest of the kernel —
likely a callback registered by whatever owns the home-blocks logic, not xHCI code
calling into block_subsystem.c directly (matching the existing "kernel/Artemis
@@ -3319,8 +3320,43 @@ no debugging needed. `logs/20260822-113852/amd64/`, `logs/20260822-114043/aarch6
Still open: correlating Command Completion Events via the real Command TRB Pointer (not needed
yet, see above), device descriptor read (2f), USB2 port-reset handling (untested — this
increment's test device was SuperSpeed and self-enabled), disconnect teardown (Disable Slot
command, DCBAA entry clear), and the callback surface into Section U's higher-level code.
increment's test device was SuperSpeed and self-enabled), and the callback surface into
Section U's higher-level code.
**Disconnect teardown (Disable Slot command, DCBAA entry clear), done 2026-08-25.** New
`XHCI_CONN_AWAIT_DISABLE_SLOT` state and `dev->pending_disable_slot_id`, same
single-outstanding-command pattern as Enable Slot/Address Device above. On a real disconnect
(`PORTSC.CCS` clear), `port_slot_id[port_id-1]` is captured into
`pending_disable_slot_id` and cleared from the tracking array *immediately* — before the
Disable Slot command's completion is known — so a fresh connect on the same port during the
brief in-flight window isn't confused for one already in progress; the command's own
completion handler is the only thing that later touches `DCBAA[slot_id]`, zeroing it on
success only (a failure completion leaves it alone rather than zeroing a context pointer the
controller might still consider live). If a disconnect arrives while another Command Ring
command is already outstanding, the Disable Slot is dropped rather than queued — same
documented limitation as Enable Slot's "already pending" case, not a new gap.
Verified live via QMP hotplug, all three architectures — a real connect→full-enumeration→
disconnect cycle exercised the new teardown path directly, not a synthetic disconnect: `xhci:
port status change -- device connected` through `xhci: device configured` (full 2f sequence)
followed immediately by `xhci: port status change -- device disconnected`, `xhci: disable slot
command submitted`, `xhci: disable slot succeeded`, no wedge. `logs/20260825-081058/amd64/`,
`logs/20260825-081300/aarch64/`, `logs/20260825-081517/riscv64/`.
**A documentation near-miss caught before commit, worth recording.** The first draft of this
writeup cited the probe-free three-arch acceptance boots (`logs/20260825-075759/amd64/` etc.)
as the verification evidence for this feature — those boots never had a USB device attached at
all (`grep xhci` on them shows only the four controller-bring-up lines, nothing past that), so
the claim was false. The mixup came from an earlier monitoring mistake in-session: a
leftover, still-running QEMU process from before a session crash left a *stale* log directory
on disk, and a `ls -dt | head -1` glob picked that stale directory instead of the freshly
launched run's — the stale log happened to already contain a full connect/enumerate/disconnect
sequence from whatever testing produced it originally, which read as this session's own result
until checked against the actual PID and the actual new log path. Caught by re-grepping the
three acceptance logs directly before writing this paragraph and finding them empty of device
activity; fixed by re-running the verification as a real, deliberate QMP hotplug (not relying
on whatever happened to be attached at boot) against freshly launched, individually-tracked
instances.
**2f. USB device enumeration (post-connect, before it's usable as storage)**
- [x] Request and parse the device descriptor — **done 2026-08-22**, see writeup below.
@@ -3415,7 +3451,11 @@ arch afterward, no wedge. `logs/20260825-073235/amd64/`, `logs/20260825-073417/a
`logs/20260825-073728/riscv64/`.
**2g. Bulk-Only Transport (BOT) — the actual read/write path**
- [ ] Identify and configure the device's bulk IN and bulk OUT endpoints
- [~] Identify the device's bulk IN and bulk OUT endpoints — **identification done
2026-08-25**, see writeup below; *configuring* them (a Configure Endpoint command,
wiring their EP Contexts/DCIs so the controller will actually run transfers on them)
is still open — this increment only reads the addresses out of the descriptor, it
doesn't yet act on them
- [ ] Implement CBW (Command Block Wrapper) construction and send, for a SCSI READ(10)
- [ ] Implement CSW (Command Status Wrapper) receive and status check
- [ ] Get one real SCSI READ(10) working end to end — first proof the whole stack works,
@@ -3425,6 +3465,34 @@ arch afterward, no wedge. `logs/20260825-073235/amd64/`, `logs/20260825-073417/a
- [ ] Implement basic error/stall recovery (CSW failure status, endpoint stall clear) — at
minimum enough to not wedge the controller on a single bad transfer
**Bulk endpoint identification, done 2026-08-25.** Extends the Interface-descriptor walk
already built in 2f: once a Mass Storage/SCSI/BOT interface is confirmed, a second, nested
walk continues forward from that interface's own offset through the Endpoint descriptors that
follow it (`bDescriptorType == 5`, USB 2.0 spec table 9-13), stopping at the next Interface
descriptor or end of the concatenated descriptor stream — matching the same "found by type,
not assumed at a fixed offset" discipline the outer walk already uses. Only endpoints whose
`bmAttributes` bits 1:0 read Bulk (`0x02`) are kept; `bEndpointAddress` bit 7 picks IN vs. OUT.
Four new `xhci_dev_t` fields (`bulk_in_ep_addr`/`bulk_in_max_packet`/`bulk_out_ep_addr`/
`bulk_out_max_packet`) hold the result — address `0` doubles as the "not found yet" sentinel
for both, since endpoint address 0 is always EP0 and can never legitimately be a bulk
endpoint's own address. A device confirmed BOT but missing either direction logs a warning
rather than failing silently.
This work was already scaffolded — the four struct fields, the descriptor-offset macros, and
their doc comments were all written and uncommitted before an earlier session ended abruptly
mid-edit — but the walk itself, the piece that actually populates them, hadn't been written
yet; this increment is that missing piece (plus resetting the four fields in
`xhci_bringup()`, which the scaffolding had also missed).
Verified live via QMP hotplug (deliberate `device_add`/`device_del` against a freshly booted,
individually-tracked instance — see the disconnect-teardown writeup above for why this
matters), all three architectures, byte-identical results everywhere: `bulk IN
endpoint=0x81`, `bulk OUT endpoint=0x02`, immediately followed by the existing `set
configuration submitted` → `device configured` chain, unaffected. `logs/20260825-081058/amd64/`,
`logs/20260825-081300/aarch64/`, `logs/20260825-081517/riscv64/`. *Configuring* these
endpoints (a Configure Endpoint command, EP Context setup) is the next open item in this
milestone — this increment only identifies them.
**2h. Integration with the existing block subsystem**
- [ ] Wire a working USB MSC device into `blk_subsys_attach_device()` (or
`blk_subsys_add_raw_device()`, whichever fits — confirm which, since USB is
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-25T11:37:26Z -->
<!-- Generated by mkcapsule --manifest 2026-08-25T12:03:09Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+14
View File
@@ -192,6 +192,7 @@ typedef struct {
/* TRB types used by this driver (subset — xHCI defines many more) */
#define XHCI_TRB_TYPE_LINK 6 /* ring-wraparound marker, Command/Transfer Rings only */
#define XHCI_TRB_TYPE_ENABLE_SLOT_CMD 9
#define XHCI_TRB_TYPE_DISABLE_SLOT_CMD 10
#define XHCI_TRB_TYPE_ADDRESS_DEVICE_CMD 11
#define XHCI_TRB_TYPE_SETUP_STAGE 2 /* Transfer Ring, control transfers only */
#define XHCI_TRB_TYPE_DATA_STAGE 3
@@ -260,6 +261,19 @@ typedef struct {
#define USB_SUBCLASS_SCSI 0x06u /* SCSI transparent command set */
#define USB_PROTOCOL_BOT 0x50u /* Bulk-Only Transport */
/* Standard USB Endpoint descriptor field offsets (7 bytes, USB 2.0 spec
* table 9-13) -- found the same way as the Interface descriptor: walked
* by bDescriptorType within the concatenated stream, not a fixed offset,
* since the number of endpoints on the interface isn't known up front. */
#define USB_DESC_TYPE_ENDPOINT 5u
#define USB_EP_OFF_ADDRESS 2u /* bEndpointAddress: bit 7 = direction, bits 3:0 = number */
#define USB_EP_OFF_ATTRIBUTES 3u /* bmAttributes: bits 1:0 = transfer type */
#define USB_EP_OFF_MAX_PACKET_SIZE 4u /* wMaxPacketSize, 2 bytes */
#define USB_EP_ADDR_DIR_MASK 0x80u
#define USB_EP_ADDR_NUM_MASK 0x0Fu
#define USB_EP_ATTR_TYPE_MASK 0x03u
#define USB_EP_TYPE_BULK 0x02u
/* Command Completion Event TRB layout (xHCI 1.2 spec table 6-32):
* parameter[63:4] = Command TRB Pointer, status[31:24] = Completion Code,
* status[23:0] = unused here, control[31:24] = Slot ID (Enable Slot's
+42 -1
View File
@@ -70,9 +70,21 @@ typedef struct {
enum {
XHCI_CONN_IDLE = 0,
XHCI_CONN_AWAIT_ENABLE_SLOT,
XHCI_CONN_AWAIT_ADDRESS_DEVICE
XHCI_CONN_AWAIT_ADDRESS_DEVICE,
XHCI_CONN_AWAIT_DISABLE_SLOT
} connect_state;
uint32_t pending_connect_slot_id;
/* Milestone 2e/2g: disconnect teardown. Same single-outstanding-
* command assumption as Enable Slot/Address Device above -- a
* disconnect that arrives while another Command Ring command is
* already outstanding is dropped rather than queued (matches the
* existing "enable slot already pending -- dropped" precedent).
* pending_disable_slot_id is captured at disconnect time, since the
* port's own tracked slot ID (port_slot_id[]) is cleared immediately
* on disconnect so a fresh connect on the same port isn't confused
* for one already in progress -- by the time the Disable Slot
* command's completion arrives, the port array no longer has it. */
uint32_t pending_disable_slot_id;
void *input_ctx; /* Input Control Ctx + Slot Ctx + EP0 Ctx (96 bytes, 32-byte contexts) */
void *device_ctx; /* Slot Ctx + EP0 Ctx (64 bytes) -- DCBAA[slot_id] points here */
xhci_trb_t *ep0_ring; /* EP0 Transfer Ring, XHCI_RING_TRB_COUNT TRBs */
@@ -107,6 +119,19 @@ typedef struct {
uint8_t config_descriptor[128];
uint16_t config_total_length;
/* Milestone 2g: bulk endpoints, discovered by walking the Endpoint
* descriptors that follow the confirmed Mass Storage/BOT Interface
* descriptor in config_descriptor. bEndpointAddress in full (not just
* the endpoint number) -- bit 7 is needed later to pick the right
* Doorbell target / EP Context DCI, and callers that want direction
* alone can just mask it. 0 means "not found yet" for both --
* endpoint address 0 is always EP0 (control), never a valid bulk
* endpoint address, so it's a safe not-found sentinel. */
uint8_t bulk_in_ep_addr;
uint16_t bulk_in_max_packet;
uint8_t bulk_out_ep_addr;
uint16_t bulk_out_max_packet;
/* Deferred chaining: a doorbell ring (new control transfer) must
* never happen synchronously from inside xhci_poll_events()'s event-
* processing loop, before ERDP has been updated for the event
@@ -209,6 +234,22 @@ void xhci_poll_events(void);
*/
int xhci_cmd_enable_slot(xhci_dev_t *dev);
/*
* xhci_cmd_disable_slot — submit a Disable Slot command TRB for slot_id
* and ring doorbell 0. Does not wait for or read
* the resulting Command Completion Event -- it
* arrives asynchronously via xhci_poll_events(),
* which clears DCBAA[slot_id] on success.
*
* Called from xhci_poll_events()'s own Port Status Change handling on a
* real disconnect event, for a slot that was actually addressed -- not
* called directly by other code.
*
* Returns 0 if the command was posted, -1 if dev/dev->cmd_ring is not set
* up.
*/
int xhci_cmd_disable_slot(xhci_dev_t *dev, uint32_t slot_id);
/*
* xhci_cmd_address_device — build the Input Context (Slot + EP0, add-only),
* program DCBAA[slot_id] with the Device Context,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+88 -5
View File
@@ -126,6 +126,7 @@ static xhci_dev_t *g_xhci_dev = NULL;
* (see the "Command Ring submission" section) so it can stay close to
* xhci_poll_events(), the read side of the same ring pair. */
int xhci_cmd_enable_slot(xhci_dev_t *dev);
int xhci_cmd_disable_slot(xhci_dev_t *dev, uint32_t slot_id);
int xhci_cmd_address_device(xhci_dev_t *dev, uint32_t slot_id,
uint32_t port_id, uint32_t speed);
int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id);
@@ -285,6 +286,7 @@ int xhci_bringup(xhci_dev_t *dev)
dev->pending_connect_speed = 0;
dev->connect_state = XHCI_CONN_IDLE;
dev->pending_connect_slot_id = 0;
dev->pending_disable_slot_id = 0;
dev->input_ctx = NULL;
dev->device_ctx = NULL;
dev->ep0_ring = NULL;
@@ -293,6 +295,10 @@ int xhci_bringup(xhci_dev_t *dev)
dev->pending_transfer_slot_id = 0;
dev->transfer_purpose = XHCI_XFER_NONE;
dev->config_total_length = 0;
dev->bulk_in_ep_addr = 0;
dev->bulk_in_max_packet = 0;
dev->bulk_out_ep_addr = 0;
dev->bulk_out_max_packet = 0;
dev->next_action = XHCI_NEXT_ACTION_NONE;
dev->next_action_slot_id = 0;
dev->next_action_length = 0;
@@ -358,6 +364,16 @@ int xhci_cmd_enable_slot(xhci_dev_t *dev)
return 0;
}
int xhci_cmd_disable_slot(xhci_dev_t *dev, uint32_t slot_id)
{
if (!dev || !dev->cmd_ring) return -1;
/* Slot ID goes in control[31:24], same field Address Device uses --
* no parameter/status payload needed, this command just names a slot. */
xhci_submit_command(dev, 0, 0, XHCI_TRB_TYPE_DISABLE_SLOT_CMD, slot_id << 24);
console_println("xhci: disable slot command submitted");
return 0;
}
/* Default EP0 Max Packet Size by PORTSC.Port Speed, used before any device
* descriptor has been read (xHCI 1.2 spec's own recommended defaults --
* the real value comes from bMaxPacketSize0 once 2f reads the device
@@ -697,12 +713,27 @@ void xhci_poll_events(void)
console_println("xhci: port status change -- device disconnected");
if (port_id >= 1 && port_id <= XHCI_MAX_TRACKED_PORTS &&
dev->port_slot_id[port_id - 1] != 0) {
/* Real teardown (Disable Slot command, DCBAA entry
* clear, callback to Section U's code) is a later
* increment -- for now just stop tracking the slot
* so a future connect on this port isn't confused
* for one already in progress. */
uint32_t disconnecting_slot_id = dev->port_slot_id[port_id - 1];
/* Stop tracking the slot immediately so a future
* connect on this port isn't confused for one
* already in progress -- independent of whether
* the Disable Slot command below can be issued
* right now. */
dev->port_slot_id[port_id - 1] = 0;
if (dev->connect_state == XHCI_CONN_IDLE) {
dev->pending_disable_slot_id = disconnecting_slot_id;
dev->connect_state = XHCI_CONN_AWAIT_DISABLE_SLOT;
xhci_cmd_disable_slot(dev, disconnecting_slot_id);
} else {
/* Same single-outstanding-command limitation
* as Enable Slot above -- the slot's DCBAA
* entry is simply left stale (harmless: it is
* never looked at again since port_slot_id[]
* no longer references it, and a genuinely
* concurrent connect/disconnect pair isn't
* this driver's current scope). */
console_println("xhci: disable slot skipped -- command ring busy");
}
}
}
/* Acknowledge only CSC (RW1CS): preserve PP, write 0 for
@@ -758,6 +789,22 @@ void xhci_poll_events(void)
}
dev->connect_state = XHCI_CONN_IDLE;
dev->pending_connect_port_id = 0;
} else if (dev->connect_state == XHCI_CONN_AWAIT_DISABLE_SLOT) {
if (code == XHCI_COMPLETION_CODE_SUCCESS) {
/* DCBAA[slot_id] cleared on success only -- if the
* controller reports failure, leave it: the slot
* may still be in a state where zeroing its
* context pointer out from under the controller
* is unsafe, and port_slot_id[] no longer
* references this slot either way, so nothing
* else in this driver will look at it again. */
((uint64_t *)dev->dcbaa)[dev->pending_disable_slot_id] = 0;
console_println("xhci: disable slot succeeded");
} else {
console_println("xhci: disable slot failed");
}
dev->connect_state = XHCI_CONN_IDLE;
dev->pending_disable_slot_id = 0;
} else {
console_println("xhci: command completion event");
}
@@ -844,6 +891,42 @@ void xhci_poll_events(void)
iface_subclass == USB_SUBCLASS_SCSI &&
iface_protocol == USB_PROTOCOL_BOT) {
console_println("xhci: confirmed Mass Storage / SCSI / BOT device");
/* Walk the Endpoint descriptors that
* follow this Interface descriptor,
* stopping at the next Interface
* descriptor (start of a different
* interface's endpoints) or end of
* the stream. Only bulk endpoints
* are of interest for BOT. */
uint16_t ep_off = (uint16_t)(off + desc_len);
while (ep_off + 2 <= len) {
uint8_t ep_desc_len = dev->config_descriptor[ep_off + USB_DESC_OFF_LENGTH];
uint8_t ep_desc_type = dev->config_descriptor[ep_off + USB_DESC_OFF_TYPE];
if (ep_desc_len == 0) break; /* malformed -- avoid an infinite loop */
if (ep_desc_type == USB_DESC_TYPE_INTERFACE) break;
if (ep_desc_type == USB_DESC_TYPE_ENDPOINT &&
ep_off + USB_EP_OFF_MAX_PACKET_SIZE + 1 < len) {
uint8_t ep_addr = dev->config_descriptor[ep_off + USB_EP_OFF_ADDRESS];
uint8_t ep_attr = dev->config_descriptor[ep_off + USB_EP_OFF_ATTRIBUTES];
uint16_t ep_max_packet = (uint16_t)(dev->config_descriptor[ep_off + USB_EP_OFF_MAX_PACKET_SIZE] |
((uint16_t)dev->config_descriptor[ep_off + USB_EP_OFF_MAX_PACKET_SIZE + 1] << 8));
if ((ep_attr & USB_EP_ATTR_TYPE_MASK) == USB_EP_TYPE_BULK) {
if (ep_addr & USB_EP_ADDR_DIR_MASK) {
dev->bulk_in_ep_addr = ep_addr;
dev->bulk_in_max_packet = ep_max_packet;
xhci_log_hex32("xhci: bulk IN endpoint=", ep_addr);
} else {
dev->bulk_out_ep_addr = ep_addr;
dev->bulk_out_max_packet = ep_max_packet;
xhci_log_hex32("xhci: bulk OUT endpoint=", ep_addr);
}
}
}
ep_off = (uint16_t)(ep_off + ep_desc_len);
}
if (dev->bulk_in_ep_addr == 0 || dev->bulk_out_ep_addr == 0) {
console_println("xhci: warning -- BOT device missing a bulk IN or OUT endpoint");
}
/* Deferred (see xhci_dev_t's
* next_action doc comment) rather
* than called directly here --