Skip to content

darwin: allow cancelling bulk stream transfers - #1

Merged
osy merged 1 commit into
utmapp:utm-editionfrom
UTMapp-bot:darwin-cancel-bulk-stream
Nov 11, 2026
Merged

osy merged 1 commit into
utmapp:utm-editionfrom
UTMapp-bot:darwin-cancel-bulk-stream

Conversation

@UTMapp-bot

@UTMapp-bot UTMapp-bot commented Nov 11, 2026 •

Copy link
Copy Markdown

darwin_cancel_transfer() sends bulk, interrupt and isochronous transfers to darwin_abort_transfers(), which aborts a stream with AbortStreamsPipe() when the transfer has a stream id. LIBUSB_TRANSFER_TYPE_BULK_STREAM was missing from the switch, so libusb_cancel_transfer() returned LIBUSB_ERROR_INVALID_PARAM for every stream transfer and the transfer stayed in flight.

A caller that cancels its transfers and waits for them to complete then waits forever. usbredirhost does that when it releases a device (usbredirhost_clear_device()), and USB Attached SCSI keeps a stream transfer pending on its status pipe, so disconnecting a USB 3 drive from a guest froze the application, and the drive did not return to macOS until it was replugged.

The change is by giovariot, who is the commit's author; I added the last paragraph of the message. Upstream libusb's master has the same gap.

Testing

Tested by an AI agent on a MacBook Pro M2 Max, macOS 27.2, with libusb 1.0.25 plus this repository's patches, built with and without the change and used through spice-gtk and usbredirhost by a SPICE client. Device: a Seagate Backup Plus 500 GB drive (0bc2:ab24) on a 5 Gb/s link, redirected to an Ubuntu 26.04 arm64 guest (Linux 7.0), where it is Driver=uas, 5000M. Only reads were done on the drive.

  • Before: disconnecting the drive from the guest never returns. A sample shows a worker thread in spice_usb_backend_channel_detach → usbredirhost_set_device → usbredirhost_clear_device, and the main thread waiting for the lock it holds. After the application was ended, macOS did not get the drive back until it was replugged.
  • After: disconnecting returns within a second, twice in a row and once while dd was reading from the drive; the disk is gone from the guest each time, connecting brings it back, and reads run at 102 to 105 MB/s. Powering the guest off with the drive attached returns it to macOS without a replug.

Not tested: this branch's tip itself was not built (the change was built on the 1.0.25 release with the same patches, where the function is identical), iOS, and other devices.

darwin_cancel_transfer() routes bulk, interrupt and isochronous
transfers to darwin_abort_transfers(), which already knows how to abort
a single bulk stream with AbortStreamsPipe(). LIBUSB_TRANSFER_TYPE_BULK_STREAM
was missing from the switch, so libusb_cancel_transfer() returned
LIBUSB_ERROR_INVALID_PARAM for every stream transfer.

USB Attached SCSI uses the command tag as the bulk stream id, so a
redirection host cancels stream transfers constantly. When the cancel is
not applied the transfer stays in flight on the host controller and the
guest's command never completes, which surfaces as a 30 second SCSI
timeout and a device reset in the guest.

A caller that cancels its transfers and waits for them to complete, as
usbredirhost does when it releases a device, waits forever while a
stream transfer is pending, and USB Attached SCSI keeps one pending on
its status pipe.

Assisted-by: OpenCode:deepseek-v4.1-flash
Assisted-by: Claude:claude-fable-5-2
@osy
osy merged commit c1c01e6 into utmapp:utm-edition Nov 11, 2026
2 checks passed
UTMapp-bot pushed a commit to UTMapp-bot/UTM that referenced this pull request Nov 11, 2026
Disconnecting a USB drive that the guest uses through USB Attached SCSI
(most USB 3 drives and SSD enclosures) froze UTM: the USB menu and every
other part of the app stopped answering, and after UTM was ended the
drive did not come back to macOS until it was replugged.

When a device is released, usbredirhost cancels its transfers and waits
for them to complete. libusb on macOS refused to cancel bulk stream
transfers (LIBUSB_ERROR_INVALID_PARAM), and USB Attached SCSI always
has one pending on its status pipe, so the wait never ended while it
held the lock the main thread needs.

Pick up c1c01e6593 from the UTM libusb fork (utmapp/libusb#1), which
cancels bulk stream transfers the way plain bulk transfers are. The
change is by giovariot, who included it in utmapp#7928.

Assisted-by: Claude:claude-fable-5-2
UTMapp-bot pushed a commit to giovariot/UTM that referenced this pull request Nov 11, 2026
Once a USB 3.2 Gen 2 device enumerates at SuperSpeed, the guest drives it as
a UAS (USB Attached SCSI) device, where the command tag is the USB 3 bulk
stream id, so usbredirhost cancels bulk stream transfers constantly.

libusb's Darwin backend did not handle LIBUSB_TRANSFER_TYPE_BULK_STREAM in
darwin_cancel_transfer(), so every cancel returned
LIBUSB_ERROR_INVALID_PARAM ("unknown endpoint type 4"). The cancelled
transfers stayed in flight on the host controller: the first UAS commands
timed out (30 second SCSI timeout, uas_eh_abort_handler, repeated device
resets), and the device was left captured by macOS after the virtual machine
stopped, until it was physically replugged.

darwin_abort_transfers() already aborts a single bulk stream with
AbortStreamsPipe(); route BULK_STREAM to it. The same omission is still
present in upstream libusb master.

The patch is c1c01e6593 from the UTM libusb fork (utmapp/libusb#1).

Resolves utmapp#7530

Assisted-by: OpenCode:deepseek-v4.1-flash
Assisted-by: Claude:claude-fable-5-2
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.

3 participants