Skip to content

Add USB mass storage support over xHCI bulk transfers - #3255

Merged
valentinbreiz merged 17 commits into
feature/xhci-usb-keyboardfrom
feature/usb-mass-storage
Sep 24, 2026
Merged

valentinbreiz merged 17 commits into
feature/xhci-usb-keyboardfrom
feature/usb-mass-storage

Conversation

@valentinbreiz

Copy link
Copy Markdown
Member

Stacked on #3254 (xHCI USB stack): the base is feature/xhci-usb-keyboard, to be retargeted to gen3 once that merges.

Problem

The USB stack from #3254 can only run control transfers and interrupt IN pipes, which is all a keyboard needs. USB sticks, card readers and USB disks are mass storage devices: they carry SCSI commands over bulk endpoints (the Bulk-Only Transport), so a plugged-in stick was enumerated and then left with no driver, never reached StorageManager, and did not show in DevKernel's diskinfo, lspart or mount.

Fix

xHCI bulk transfers. UsbDevice gains OpenBulkEndpoint, BulkIn, BulkOut, ResetEndpoint and ClearHalt, so class drivers stay host-controller agnostic.

  • A bulk pipe (XhciBulkPipe) is synchronous: one Normal TRB per chunk, through a DMA bounce buffer. The 16 pages are page aligned but not 64 KiB aligned, so the pipe uses the longer side of the 64 KiB boundary they may straddle (at least 32 KiB, a multiple of every packet size). Longer transfers are split into several chunks, and a short packet ends the transfer.
    • Rejected: chaining one TRB per page. Where a short packet in the middle of a TD raises its events differs between xHCI revisions (Linux carries quirks for it).
  • A failed or timed-out transfer leaves the endpoint Stopped with its ring cleared: Stop Endpoint if Running, Reset Endpoint if Halted, then Set TR Dequeue Pointer.
  • ResetEndpoint must also restart the host's data toggle. Reset Endpoint does that, but only on a Halted endpoint. Any other endpoint is dropped and added back through Configure Endpoint (Linux xhci_endpoint_reset).
  • SuperSpeed bulk endpoints take bMaxBurst from their SuperSpeed Endpoint Companion descriptor, which ParseConfiguration now reads.
  • Commands and synchronous control transfers are each serialized by a scheduler mutex. Endpoint recovery now issues them at runtime, from whichever thread hit the error, not only during the single-threaded boot enumeration.

Mass storage class driver. UsbMassStorageDriver binds interfaces with class 0x08, subclass 0x06 (SCSI transparent) and protocol 0x50 (Bulk-Only).

  • UsbBulkOnlyTransport sends the CBW, runs the data stage and reads the CSW. It clears a STALLed data stage and retries a STALLed CSW once. On a bad CSW, a phase error or a failed stage, it runs Reset Recovery (Bulk-Only Mass Storage Reset, then clear both halts).
    • When a command fails, the transport fetches the sense data itself (REQUEST SENSE) before releasing its mutex. That way another thread's command cannot clear it.
  • UsbMassStorage is a BlockDevice per logical unit, named usb0, usb1, …:
    • At bind: INQUIRY (direct-access units only), TEST UNIT READY until ready, READ CAPACITY(10), then (16) past 2^32 blocks. An empty card slot answers "medium not present" and is skipped.
    • Reads and writes use READ/WRITE(10), or (16) past 2^32 blocks, at most 64 KiB per command. A UNIT ATTENTION or a transport error is retried.
    • Flush sends SYNCHRONIZE CACHE, and stops sending it once a device rejects it with ILLEGAL REQUEST, as most flash drives do.
  • StorageManager.RegisterHalDevices registers the units after the AHCI ports and NVMe namespaces, so an internal disk stays the primary device.
  • USB now also comes up when only storage is enabled (KeyboardEnabled || StorageEnabled).

Tests. A usb disk kind attaches an image as a usb-storage stick on a shared qemu-xhci controller, both in test profiles and in cosmos run --disk image,usb.

  • The Storage suite now runs a usb profile under the same assertions as ahci and nvme. That makes 6 cells on x64 and 18 on arm64, so the arm64 step and job budgets go from 45/60 to 60/75 minutes.
  • Device_LargeTransfer now moves 257 blocks instead of 32, past every driver's per-command limit plus one. This covers splitting one request into several commands, with a short last one.

Verification

Real hardware: the author confirmed it works on the Lenovo Legion (AMD Phoenix) that #3254 was brought up on.

DevKernel, QEMU x64 (q35, blank AHCI and NVMe disks plus a 64 MiB stick holding an MBR and a FAT32 partition):

[USB] xHCI root port 2: 46F4:1 SuperSpeed, 1 interface(s)
[USB storage] usb0 (LUN 0): QEMU QEMU HARDDISK, 131072 blocks of 512 bytes
[USB] xHCI root port 2: interface 0 (class 0x8): mass storage
[StorageManager] MBR detected on usb0
[DevKernel] FAT mounted on /mnt from partition 0
cosmos:/$ diskinfo
  [2] Name         usb0
      Block Size   512 B
      Sectors      131072
      Capacity     64 MiB
      Table        MBR
cosmos:/$ lspart
      [0] usb0p0  Start=2048  Sectors=129024  63 MiB  FAT32
cosmos:/$ cat /mnt/HELLO.TXT
Hello from the USB stick!
cosmos:/$ write /mnt/cosmos.txt Written by Cosmos over USB
Wrote 26 bytes to /mnt/cosmos.txt

After shutdown, the host reads the file back from the image (mcopy -i usb.img@@1M ::/COSMOS.TXT - prints Written by Cosmos over USB). fsck.vfat -n finds no damage, only the FSInfo free-cluster hint, which the FAT driver never updates on any disk.

A second boot put a FAT disk on AHCI (so the auto-mount takes it) and added more devices:

Device Result
Same stick, SuperSpeed usb0; mount 2 0 /usb, file from the previous boot still there
Stick with no partition table, full speed, behind a usb-hub usb1, "Unpartitioned filesystem volume detected"; mount 3 0 /flop (FAT16), read and write, write checked from the host
Removable unit with no medium [USB storage] usb1 (LUN 0): no medium, skipped

DevKernel, QEMU arm64 (virt, GICv3): [xHCI] Running, events via MSI-X, usb0 bound, FAT auto-mounted. cat and write work, and the host reads Hello from ARM64 back from the image.

Storage suite (make test KERNEL=Storage):

Arch Cells Failed usb cells
x64 6 (ahci, nvme, usb × {bare, acpi-off}) 0 pass everything except the 4 NVMe-only tests (skipped)
arm64 18 (× {bare, gicv2, gicv3} × acpi-off) 0 usb, usb+gicv2 (polled, no ITS), usb+gicv3: 69 passed each, same as ahci; the acpi-off cells skip like ahci/nvme (no PCIe discovery without ACPI)

Device_LargeTransfer at 257 blocks passes in all 6 x64 cells. dotnet test tests/Cosmos.Tests.Patcher (RunCommand and QemuLauncher tests, including the new usb cases): 56 passed. dotnet format --verify-no-changes --severity error is clean on the changed files.

Known gaps:

  • No hot-plug: a stick must be plugged in before boot, like every USB device in Add an xHCI USB stack with hub and HID boot keyboard drivers #3254.
  • UAS (USB Attached SCSI) is not implemented. UAS devices still work through their Bulk-Only alternate setting 0.
  • Only the SCSI transparent subclass is bound. Units that are not direct-access disks (CD-ROM, …) are skipped.
  • Bulk waits poll the event ring, like the existing control transfers, instead of sleeping until the MSI-X interrupt.

Class drivers could only use control transfers and interrupt IN pipes,
which is all a keyboard needs; mass storage runs on bulk endpoints.

UsbDevice gains OpenBulkEndpoint, BulkIn, BulkOut, ResetEndpoint and
ClearHalt. On xHCI a bulk pipe is synchronous, one Normal TRB at a time
through a DMA bounce buffer placed so no TRB crosses a 64 KiB boundary;
longer transfers are split and a short packet ends them. A failed or
timed-out transfer leaves the endpoint Stopped with its ring cleared
(Stop Endpoint or Reset Endpoint, then Set TR Dequeue Pointer), and
ResetEndpoint also restarts the data toggle, by dropping and re-adding a
non-halted endpoint. SuperSpeed endpoints take bMaxBurst from their
companion descriptor.

Commands and synchronous control transfers are now serialized by a
mutex each: endpoint recovery issues them from whichever thread hit the
error, no longer only from the boot-time enumeration.
USB sticks, card readers and USB disks carry SCSI commands over the
Bulk-Only Transport. The driver binds those interfaces, asks for the
LUN count, and turns every unit with a medium into a block device named
usb0, usb1, ...: INQUIRY, TEST UNIT READY until ready (an empty card
slot is skipped), READ CAPACITY (10, then 16 past 2^32 blocks). Reads
and writes go out as READ/WRITE(10), or (16) past 2^32, 64 KiB at most
per command. A failed command's sense data is fetched at once; a UNIT
ATTENTION or a transport error, after which the device is reset, is
retried. Flush sends SYNCHRONIZE CACHE and stops once a device rejects
it, as most flash drives do.

StorageManager registers the units after the AHCI and NVMe devices, so
an internal disk stays the primary one, which puts them in diskinfo,
lspart and mount like any other disk. USB now also comes up when only
storage is enabled.
A usb disk kind attaches the image as a usb-storage stick on a shared
qemu-xhci controller, in test profiles and in `cosmos run --disk
image,usb`. The Storage suite runs in a new usb profile under the same
block-device assertions as ahci and nvme, which makes 6 cells on x64 and
18 on arm64; the arm64 step and job budgets grow to match.

Device_LargeTransfer now moves 257 blocks instead of 32, past every
driver's per-command limit plus one, so splitting a request into several
commands and a short last one are covered.
List USB sticks among the devices StorageManager registers, note that
they are only found at boot, and show `cosmos run --disk image,usb`.
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Fat Tests

Cell x64 arm64
all ✅ 42/42 ✅ 42/42

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 File Tests

Cell x64 arm64
all ✅ 37/37 ✅ 37/37

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 GarbageCollector Tests

Cell x64 arm64
all ✅ 47/47 ✅ 47/47

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Interrupts Tests

Cell x64 arm64
all ✅ 12/12 ✅ 25/36, 11 skip
bare ✅ 12/12 ✅ 7/12, 5 skip
bare+gicv2 n/a ✅ 9/12, 3 skip
bare+gicv3 n/a ✅ 9/12, 3 skip

Skipped

  • arm64: 11 skipped: self-IPI harness is x64-only (3), LAPIC MSI address contract is x64-only (3), LAPIC timer vector is x64-only (3), gic-version not pinned by this cell (2)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Math Tests

Cell x64 arm64
all ✅ 30/30 ✅ 30/30

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Memory Tests

Cell x64 arm64
all ✅ 71/71 ✅ 71/71

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Network Tests

Cell x64 arm64
all ✅ 48/48 ✅ 48/48
e1000e ✅ 24/24 n/a
virtio-net-mmio n/a ✅ 24/24
virtio-net-pci ✅ 24/24 ✅ 24/24

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Pci Tests

Cell x64 arm64
all ✅ 6/6 ✅ 6/6

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Power Tests

Cell x64 arm64
all ✅ 4/4 ✅ 4/4

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Runtime Tests

Cell x64 arm64
all ✅ 103/103 ✅ 103/103

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Storage Tests

Cell x64 arm64
all ✅ 426/468, 42 skip ⚠️ 668/1404, 736 skip
ahci ✅ 69/78, 9 skip ✅ 69/78, 9 skip
ahci+acpi-off ✅ 69/78, 9 skip ⚠️ 3/78, 75 skip
ahci+gicv2 n/a ✅ 69/78, 9 skip
ahci+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
ahci+gicv3 n/a ✅ 69/78, 9 skip
ahci+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip
nvme ✅ 73/78, 5 skip ✅ 70/78, 8 skip
nvme+acpi-off ✅ 72/78, 6 skip ⚠️ 3/78, 75 skip
nvme+gicv2 n/a ✅ 71/78, 7 skip
nvme+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
nvme+gicv3 n/a ✅ 71/78, 7 skip
nvme+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip
usb ✅ 74/78, 4 skip ✅ 74/78, 4 skip
usb+acpi-off ✅ 69/78, 9 skip ⚠️ 3/78, 75 skip
usb+gicv2 n/a ✅ 74/78, 4 skip
usb+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
usb+gicv3 n/a ✅ 74/78, 4 skip
usb+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip

Skipped

  • x64: 42 skipped: not a USB profile (20), 64-bit BAR relocation probe is nvme-profile only (8), USB hot-plug thread not running (scheduler timer not ticking) (5), not an NVMe profile (4), NVMe controller API is nvme-profile only (4), acpi-off has no MSI routing to pin (1)
  • arm64: 736 skipped: no block device bound for partition-table tests (441), no block device bound for this profile (171), not a USB profile (60), x64 mapper cell; arm64 installs Device mappings via DeviceMapper (18), BAR relocation probe is x64-only (same harness as the mapper cell) (18), NVMe controller API is nvme-profile only (15), not an NVMe profile (12), interrupt mode not pinned by this cell (1)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Timer Tests

Cell x64 arm64
all ✅ 26/26 ✅ 20/20

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 TypeCasting Tests

Cell x64 arm64
all ✅ 17/17 ✅ 17/17

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🧪 Virtio Tests

Cell x64 arm64
all ✅ 10/10 ✅ 17/20, 3 skip
virtio-mmio n/a ✅ 7/10, 3 skip
virtio-pci ✅ 10/10 ✅ 10/10

Skipped

  • arm64: 3 skipped: this cell presents virtio over MMIO (3)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

A hot-plug thread, started once the scheduler's timer ticks, waits for
port changes: the xHCI Port Status Change Event on a root port, the
status change endpoint on a hub port, or a 250 ms poll when the
controller has no MSI-X. It clears the change bits, drops whatever sat
on a port that disconnected, and after a 100 ms debounce enumerates what
was plugged in.

A device that leaves is marked disconnected from the interrupt handler
already, so transfers waiting on it return the new Disconnected status
instead of timing out and skip recovery. Its subtree is then released
children first: each class driver's Disconnect, the device leaving the
manager's list, and the controller disabling its slot once no control or
bulk transfer still holds its memory.

Hub port handling moves into UsbHub, which also follows the hub's status
change endpoint. The mass storage and keyboard drivers keep copy-on-write
lists and report units that come and go through attach/detach callbacks.
I/O on a removed stick throws IOException at once, and names are reused
from the lowest free usbN. Without the scheduler's timer (x64 with ACPI
off) hot-plug stays off and the devices are the ones found at boot.
Kernel.Start starts the USB hot-plug thread once interrupts are on. A
stick that shows up is registered and its partitions scanned like one
found at boot; one that goes away is unregistered with its partitions,
the primary device moves on, and every filesystem mounted from one of
its partitions is detached without a flush, since the disk is gone. USB
keyboards join and leave KeyboardManager the same way.

The device, partition, mount and keyboard tables are replaced whole on
every change instead of changed in place, so a reader on another thread
always sees a consistent list. Partition scans run outside the lock and
are published only if their device is still registered. TryUnmount
drops the mount outside the lock, and TryFormat refuses a partition that
is mounted.
`umount <mountpoint>` unmounts and moves the shell out of it. An I/O
error, such as a stick pulled out under a running command, is reported
and the shell keeps going. The boot stick is auto-mounted through its
partition, so it is detached if pulled out, and an empty disk list now
says a USB disk can be plugged in.
USB sticks and keyboards can be plugged in and pulled out while the
kernel runs. Explain what happens to mounts (partition mounts are
detached, source-string mounts stay), to unmount before pulling a
stick, how to try it from the QEMU monitor, and where Kernel.Start
starts the hot-plug thread.
TR.RequestHost sends a new HostRequest frame (109), and the engine acts
on it as soon as it shows up on the UART: `usb-unplug [n]` removes stick
n from the xHCI controller with device_del, and `usb-plug [n]` puts it
back on the same image with drive_add and device_add. It gets there
through QMP: a profile with a USB disk launches QEMU with its monitor
connected to a port the engine listens on (QemuLaunchOptions.MonitorPort),
and USB sticks get stable QEMU ids. Nothing replies; the test waits for
the change, within the 10 s the engine gives it before taking the guest
for hung.
Five tests in the usb cells pull the stick out and plug it back in:
the stick leaves StorageManager and the driver, I/O on it throws
IOException, it comes back under its old name with the sector written
before the unplug, and a filesystem mounted from its partition is
detached with it, then mounts again with its file intact. They skip on
other cells and where the hot-plug thread cannot run (x64 with ACPI
off).
Bring the files this branch adds or changes in line with
docs/articles/dev/coding-guidelines.md. No behavior changes.

- Move constructors after fields and properties.
- Split UsbEndpointType into its own file.
- Replace pass-through fields with get-only auto-properties.
- Use explicit types instead of var, is null / is not null instead of
  == null / != null, interpolation instead of concatenation, and
  collection expressions.
- Add license headers and a <summary> to QemuHotPlug.DisposeAsync.
- Rename HostRequestScanner.Magic to s_magic.
- Remove comments that only restate the code.
- Explain why the USB drivers' lazily filled statics stay null.
Track .claude/agents/ so every contributor gets the code-cleaner agent,
which reads docs/articles/dev/coding-guidelines.md and cleans source
files to it without changing behavior. The rest of .claude, such as
settings.local.json, stays ignored.
The parser's range check stopped at TestDestructiveReached (108), so
HostRequest (109) frames were skipped as noise and their case never ran.
DisposeAsync waited for the monitor connection before it stopped the
listener. If QEMU exited before connecting, for example because it
rejected an argument, the accept never completed: its token belongs to a
source the host disposes without cancelling. The engine then hung at
await using. Stop the listener first, which ends the pending accept.
IndexOf and LastIndexOf both return -1 when qemu-xhci is missing, so the
one-controller check passed with no controller at all. Assert that the
controller is there too.
Move USB support from the future releases to the Gen3 features, with
what it covers now and what it doesn't yet (USB mouse, EHCI). Drop the
first-release percentage badge.
Add USB hot-plug for sticks, hubs and keyboards
@valentinbreiz
valentinbreiz merged commit 2e6188b into feature/xhci-usb-keyboard Sep 24, 2026
26 checks passed
@valentinbreiz
valentinbreiz deleted the feature/usb-mass-storage branch September 24, 2026 08:06
@github-actions

Copy link
Copy Markdown

🧪 HelloWorld Tests

Cell x64 arm64
all ✅ 3/3 ✅ 3/3

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Threading Tests

Cell x64 arm64
all ✅ 76/76 ✅ 76/76

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant