An ISO OUT reply claimed zero bytes consumed, and the kernel dropped the whole device #286

Merged
enricobuehler merged 1 commits from worktree-usbip-iso-actual-length into main 2026-08-17 12:59:59 +00:00
1 Commits
Author SHA1 Message Date
enricobuehler 1c9aec35fe fix(usbip): an ISO OUT reply claimed zero bytes consumed, and the kernel dropped the whole device
ci / bun-nix (pull_request) Successful in 24s
ci / web (pull_request) Successful in 1m12s
ci / docs-site (pull_request) Successful in 1m19s
ci / rust-arm64 (pull_request) Successful in 5m38s
android / android (pull_request) Successful in 5m40s
ci / rust (pull_request) Successful in 15m13s
Turning on PUNKTFUNK_DUALSENSE_USBIP=1 made the DualSense vanish from the host entirely - unusable in game and absent from Steam's controller list. The usbip path replaces uhid, so there is no fallback.

Enumeration was never the problem: the kernel bound both drivers to the virtual pad ('playstation ... hidraw6: USB HID v1.11 Gamepad [Sony Interactive Entertainment DualSense Wireless Controller]' and snd-usb-audio processing the mixer). ~56 ms later, as soon as snd-usb-audio submitted the first isochronous OUT URB, the link tore down:

    usb 11-1: recv xbuf, 0
    vhci_hcd: sendmsg failed!, ret=-32 for 48
    vhci_hcd vhci_hcd.0: stop threads / release socket / disconnect device

usbip_ret_submit_iso filled each packet descriptor's actual_length correctly (the bytes accepted, = the requested length on OUT) but set the URB-level actual_length from transfer_buffer.len(), and the transfer buffer is only populated INBOUND. So an OUT reply stated per-packet actuals summing to N x 392 against a total of 0.

The kernel's usbip_recv_iso() sums the packet table and compares it to the URB's actual_length; on a mismatch it raises ERROR_TCP and returns -EPIPE, which drops the connection rather than the URB - hence the pad disappearing outright and the attach loop retrying forever. The function's own doc comment already stated the required semantics; only the aggregate missed them.

actual_length is now the running total of the per-packet actuals, which is unchanged for IN (where it equals the buffer) and correct for OUT. to_bytes()'s debug_assert encoded the same wrong rule for ISO and now checks the kernel's invariant directly, against the descriptor table.

The OUT unit test asserted actual_length == 0 - it had locked in the bug, conflating 'sends no payload back' with 'consumed nothing'. It now pins the real invariant, sum(table actual_length) == actual_length.

Not yet re-tested on hardware.
2026-08-17 14:57:25 +02:00