Artemis Milestone 2f: Configuration descriptor read + Mass Storage/BOT class confirmation

Chains off the device descriptor request via a new deferred-action mechanism
on xhci_dev_t (next_action/next_action_slot_id/next_action_length): a short
9-byte Configuration descriptor read learns wTotalLength, then a full read
retrieves Config+Interface+Endpoint descriptors, walked for the Interface
descriptor to confirm bInterfaceClass/SubClass/Protocol == Mass Storage/
SCSI/Bulk-Only Transport.

The deferral exists because ringing the next doorbell synchronously inside
xhci_poll_events()'s event-processing loop -- before the current event's
ERDP write -- hung the guest outright (confirmed live via checkpoint
logging, amd64). Fixed by moving the actual control-transfer submission to
a small dispatch at the end of xhci_poll_events(), after ERDP is updated.

A debug hack that shipped mid-session (forcing a repeated 9-byte read
instead of chaining into the real 44-byte length, to isolate whether the
hang was doorbell-ordering or length-specific) has been reverted: restored
the real length and re-verified live. The doorbell-ordering fix was the
whole story -- the 44-byte read completes cleanly.

Verified live via QMP hotplug, all three architectures, byte-identical
results: wTotalLength=0x2c, bInterfaceClass=0x08, bInterfaceSubClass=0x06,
bInterfaceProtocol=0x50 -- confirmed Mass Storage/SCSI/BOT. Disconnect
confirmed clean on every arch, no wedge. FABRIC-2.md Section X Milestone 2f
updated with the full writeup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPfdtaXs9ay1nbwuMnrscu
This commit is contained in:
Robert Allan James
2026-08-25 07:27:07 -04:00
co-authored by Claude Sonnet 5
parent 2c34e45d05
commit b9c540a78b
22 changed files with 101974 additions and 52 deletions
+176 -37
View File
@@ -129,6 +129,7 @@ int xhci_cmd_enable_slot(xhci_dev_t *dev);
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);
int xhci_ep0_get_config_descriptor(xhci_dev_t *dev, uint32_t slot_id, uint16_t length);
int xhci_bringup(xhci_dev_t *dev)
{
@@ -289,6 +290,11 @@ int xhci_bringup(xhci_dev_t *dev)
dev->ep0_ring_cycle = 1;
dev->ep0_ring_enq = 0;
dev->pending_transfer_slot_id = 0;
dev->transfer_purpose = XHCI_XFER_NONE;
dev->config_total_length = 0;
dev->next_action = XHCI_NEXT_ACTION_NONE;
dev->next_action_slot_id = 0;
dev->next_action_length = 0;
console_println("xhci: controller running");
/* Milestone 2e prep: HCCPARAMS1.CSZ decides 32- vs 64-byte Slot/
@@ -476,21 +482,25 @@ static void xhci_ep0_enqueue_trb(xhci_dev_t *dev, uint64_t parameter,
}
}
int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id)
/* Shared submission for any "device-to-host, standard, device recipient,
* IN data stage" control read -- both GET_DESCRIPTOR(Device) and
* GET_DESCRIPTOR(Configuration) are this same shape, differing only in
* wValue/wLength/destination buffer. Does not set dev->transfer_purpose
* or dev->pending_transfer_slot_id -- callers do that themselves so the
* purpose is set before the doorbell rings (avoids a window where a
* stray Transfer Event could be misread against a not-yet-set purpose,
* even though this driver is polled and that window can't actually be
* hit by anything external in practice). */
static void xhci_ep0_control_read(xhci_dev_t *dev, uint8_t bRequest,
uint16_t wValue, uint16_t wIndex,
uint8_t *buf, uint16_t len)
{
if (!dev || !dev->ep0_ring) return -1;
/* Standard GET_DESCRIPTOR(Device) request (USB 2.0 spec section
* 9.4.3): device-to-host, standard, device recipient; wValue high
* byte selects descriptor type, low byte the index (0 for the one
* Device descriptor); wLength 18 = the full standard Device
* descriptor size. */
usb_setup_packet_t setup = {
.bmRequestType = USB_DIR_DEVICE_TO_HOST,
.bRequest = USB_REQ_GET_DESCRIPTOR,
.wValue = (uint16_t)(USB_DESC_TYPE_DEVICE << 8),
.wIndex = 0,
.wLength = sizeof(dev->device_descriptor)
.bRequest = bRequest,
.wValue = wValue,
.wIndex = wIndex,
.wLength = len
};
uint64_t setup_bits;
memcpy(&setup_bits, &setup, sizeof(setup_bits));
@@ -503,10 +513,9 @@ int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id)
(XHCI_SETUP_TRT_IN_DATA << XHCI_TRB_CONTROL_TRT_SHIFT));
/* Data Stage: parameter is a real pointer here (not immediate) --
* points at the reused device_descriptor buffer. DIR=IN matches the
* Setup Stage's TRT. */
xhci_ep0_enqueue_trb(dev, (uint64_t)(uintptr_t)dev->device_descriptor,
(uint32_t)sizeof(dev->device_descriptor),
* points at the caller's buffer. DIR=IN matches the Setup Stage's
* TRT. */
xhci_ep0_enqueue_trb(dev, (uint64_t)(uintptr_t)buf, len,
(XHCI_TRB_TYPE_DATA_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_DIR_IN);
@@ -517,6 +526,16 @@ int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id)
xhci_ep0_enqueue_trb(dev, 0, 0,
(XHCI_TRB_TYPE_STATUS_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IOC);
}
int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id)
{
if (!dev || !dev->ep0_ring) return -1;
dev->transfer_purpose = XHCI_XFER_DEVICE_DESC;
xhci_ep0_control_read(dev, USB_REQ_GET_DESCRIPTOR,
(uint16_t)(USB_DESC_TYPE_DEVICE << 8), 0,
dev->device_descriptor, sizeof(dev->device_descriptor));
dev->pending_transfer_slot_id = slot_id;
/* Doorbell Array is indexed by slot ID; target 1 = Default Control
@@ -528,6 +547,31 @@ int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id)
return 0;
}
int xhci_ep0_get_config_descriptor(xhci_dev_t *dev, uint32_t slot_id, uint16_t length)
{
if (!dev || !dev->ep0_ring) return -1;
/* Cap to the fixed buffer size -- a device whose real Configuration
* descriptor set exceeds this would be truncated, not overflowed;
* 128 bytes comfortably covers a single-interface Mass Storage
* device (Config 9 + Interface 9 + 2 Endpoints * 7 = 32 bytes
* typical), so this is a defensive cap, not an expected path. */
if (length > sizeof(dev->config_descriptor)) {
length = (uint16_t)sizeof(dev->config_descriptor);
}
dev->transfer_purpose = (length <= 9) ? XHCI_XFER_CONFIG_DESC_SHORT
: XHCI_XFER_CONFIG_DESC_FULL;
xhci_ep0_control_read(dev, USB_REQ_GET_DESCRIPTOR,
(uint16_t)(USB_DESC_TYPE_CONFIG << 8), 0,
dev->config_descriptor, length);
dev->pending_transfer_slot_id = slot_id;
dev->doorbell[slot_id] = XHCI_DB_TARGET(1);
console_println("xhci: get config descriptor submitted");
return 0;
}
/* -------------------------------------------------------------------------
* Milestone 2d: Event Ring servicing, polled from sk_repl_idle().
*
@@ -659,10 +703,11 @@ void xhci_poll_events(void)
console_println("xhci: address device succeeded");
/* Milestone 2f: enumeration starts here -- the
* device now has a USB address and EP0 is
* usable for control transfers. */
if (xhci_ep0_get_device_descriptor(dev, dev->pending_connect_slot_id) != 0) {
console_println("xhci: device descriptor request setup failed");
}
* usable for control transfers. Deferred (see
* xhci_dev_t's doc comment on next_action) rather
* than called directly here. */
dev->next_action = XHCI_NEXT_ACTION_GET_DEVICE_DESC;
dev->next_action_slot_id = dev->pending_connect_slot_id;
} else {
console_println("xhci: address device failed");
}
@@ -676,24 +721,100 @@ void xhci_poll_events(void)
case XHCI_TRB_TYPE_TRANSFER_EVENT: {
uint32_t code = XHCI_EVT_COMPLETION_CODE(trb->status);
if (dev->pending_transfer_slot_id != 0) {
uint32_t xfer_slot_id = dev->pending_transfer_slot_id;
uint32_t purpose = dev->transfer_purpose;
dev->pending_transfer_slot_id = 0;
if (code == XHCI_COMPLETION_CODE_SUCCESS) {
console_println("xhci: device descriptor received");
/* USB 2.0 spec table 9-8 layout. Logged, not yet
* acted on -- 2f's own punch list asks whether
* vendor/product IDs are even needed for this
* project, or class-only detection suffices;
* this surfaces the real values to help decide,
* doesn't decide it here. */
uint32_t id_vendor = dev->device_descriptor[8] |
((uint32_t)dev->device_descriptor[9] << 8);
uint32_t id_product = dev->device_descriptor[10] |
((uint32_t)dev->device_descriptor[11] << 8);
xhci_log_hex32("xhci: idVendor=", id_vendor);
xhci_log_hex32("xhci: idProduct=", id_product);
xhci_log_hex32("xhci: bDeviceClass=", dev->device_descriptor[4]);
} else {
console_println("xhci: device descriptor request failed");
dev->transfer_purpose = XHCI_XFER_NONE;
if (code != XHCI_COMPLETION_CODE_SUCCESS) {
console_println("xhci: control transfer failed");
break;
}
switch (purpose) {
case XHCI_XFER_DEVICE_DESC: {
console_println("xhci: device descriptor received");
/* USB 2.0 spec table 9-8 layout. Logged --
* 2f's own punch list asked whether vendor/
* product IDs are even needed, or class-only
* detection suffices; this surfaces the real
* values, doesn't decide it. */
uint32_t id_vendor = dev->device_descriptor[8] |
((uint32_t)dev->device_descriptor[9] << 8);
uint32_t id_product = dev->device_descriptor[10] |
((uint32_t)dev->device_descriptor[11] << 8);
xhci_log_hex32("xhci: idVendor=", id_vendor);
xhci_log_hex32("xhci: idProduct=", id_product);
xhci_log_hex32("xhci: bDeviceClass=", dev->device_descriptor[4]);
/* Chain: request just the Configuration
* descriptor's 9-byte header first, to learn
* wTotalLength before requesting everything.
* Deferred (see xhci_dev_t's doc comment on
* next_action) rather than called directly --
* a doorbell rung synchronously here, still
* inside this event-processing loop and
* before ERDP is updated, hung the guest
* outright (confirmed live via checkpoint
* logging, amd64 QEMU, 2026-08-22). */
dev->next_action = XHCI_NEXT_ACTION_GET_CONFIG_DESC;
dev->next_action_slot_id = xfer_slot_id;
dev->next_action_length = 9;
break;
}
case XHCI_XFER_CONFIG_DESC_SHORT: {
uint16_t total_len = (uint16_t)(dev->config_descriptor[USB_CONFIG_OFF_TOTAL_LENGTH] |
((uint16_t)dev->config_descriptor[USB_CONFIG_OFF_TOTAL_LENGTH + 1] << 8));
dev->config_total_length = total_len;
xhci_log_hex32("xhci: config wTotalLength=", total_len);
dev->next_action = XHCI_NEXT_ACTION_GET_CONFIG_DESC;
dev->next_action_slot_id = xfer_slot_id;
dev->next_action_length = total_len;
break;
}
case XHCI_XFER_CONFIG_DESC_FULL: {
console_println("xhci: full config descriptor received");
/* Walk the concatenated descriptor stream
* (Config + Interface + Endpoint descriptors
* back to back) looking for the Interface
* descriptor -- its fixed offset within the
* stream isn't guaranteed, has to be found by
* bDescriptorType, not assumed. */
uint16_t len = dev->config_total_length;
if (len > sizeof(dev->config_descriptor)) len = (uint16_t)sizeof(dev->config_descriptor);
uint16_t off = 0;
int found = 0;
while (off + 2 <= len) {
uint8_t desc_len = dev->config_descriptor[off + USB_DESC_OFF_LENGTH];
uint8_t desc_type = dev->config_descriptor[off + USB_DESC_OFF_TYPE];
if (desc_len == 0) break; /* malformed -- avoid an infinite loop */
if (desc_type == USB_DESC_TYPE_INTERFACE &&
off + USB_IFACE_OFF_PROTOCOL < len) {
uint8_t iface_class = dev->config_descriptor[off + USB_IFACE_OFF_CLASS];
uint8_t iface_subclass = dev->config_descriptor[off + USB_IFACE_OFF_SUBCLASS];
uint8_t iface_protocol = dev->config_descriptor[off + USB_IFACE_OFF_PROTOCOL];
xhci_log_hex32("xhci: bInterfaceClass=", iface_class);
xhci_log_hex32("xhci: bInterfaceSubClass=", iface_subclass);
xhci_log_hex32("xhci: bInterfaceProtocol=", iface_protocol);
if (iface_class == USB_CLASS_MASS_STORAGE &&
iface_subclass == USB_SUBCLASS_SCSI &&
iface_protocol == USB_PROTOCOL_BOT) {
console_println("xhci: confirmed Mass Storage / SCSI / BOT device");
} else {
console_println("xhci: not a Mass Storage/SCSI/BOT device -- not usable as a drive");
}
found = 1;
break;
}
off = (uint16_t)(off + desc_len);
}
if (!found) {
console_println("xhci: no Interface descriptor found in config set");
}
break;
}
default:
console_println("xhci: transfer event");
break;
}
} else {
console_println("xhci: transfer event");
@@ -723,4 +844,22 @@ void xhci_poll_events(void)
* have nothing to clear. */
dev->intr0->erdp = ((uint64_t)(uintptr_t)&dev->evt_ring[dev->evt_ring_deq]
& XHCI_ERDP_PTR_MASK) | XHCI_ERDP_EHB;
/* Deferred chained request, if event processing above set one --
* see xhci_dev_t's own doc comment on why this must happen here,
* after ERDP is updated, not synchronously inside the loop above. */
if (dev->next_action == XHCI_NEXT_ACTION_GET_DEVICE_DESC) {
uint32_t next_slot_id = dev->next_action_slot_id;
dev->next_action = XHCI_NEXT_ACTION_NONE;
if (xhci_ep0_get_device_descriptor(dev, next_slot_id) != 0) {
console_println("xhci: deferred device descriptor request setup failed");
}
} else if (dev->next_action == XHCI_NEXT_ACTION_GET_CONFIG_DESC) {
uint32_t next_slot_id = dev->next_action_slot_id;
uint16_t next_length = dev->next_action_length;
dev->next_action = XHCI_NEXT_ACTION_NONE;
if (xhci_ep0_get_config_descriptor(dev, next_slot_id, next_length) != 0) {
console_println("xhci: deferred config descriptor request setup failed");
}
}
}