Fixes the field failure from turning on PUNKTFUNK_DUALSENSE_USBIP=1 (#282): the DualSense vanished from the host entirely — unusable in game and absent from Steam's controller list. The usbip path replaces uhid, so a failure leaves no pad at all.
Enumeration was never the problem
The kernel bound both drivers to the virtual pad:
usb 11-1: 2:0: sticky mixer values (-15360/0/48 => 0), disabling ← snd-usb-audio bound
playstation 0003:054C:0CE6.0011: hidraw6: USB HID v1.11 Gamepad
[Sony Interactive Entertainment DualSense Wireless Controller]
on usb-vhci_hcd.0-1/input3 ← hid-playstation bound
So the composite descriptor set, the UAC function and the 054C:0CE6 identity are all accepted — the load-bearing assumption of #282 holds. ~56 ms later, as soon as snd-usb-audio submitted the first isochronous OUT URB:
usbip_ret_submit_iso filled each packet descriptor'sactual_length correctly (bytes accepted — the requested length on OUT), but set the URB-levelactual_length from transfer_buffer.len(), and the transfer buffer is only populated inbound. An OUT reply therefore stated per-packet actuals summing to N × 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, not the URB. Hence the pad disappearing outright, and the attach loop retrying forever.
The function's own doc comment already stated the required semantics — "On an OUT endpoint there is no payload to return and the value is the number of bytes we accepted… Reporting the empty reply's length there would tell the kernel the device swallowed nothing" — only the aggregate missed them.
The fix
actual_length is now the running total of the per-packet actuals: unchanged for IN (where it already equalled the buffer length) and correct for OUT.
to_bytes()'s debug_assert encoded the same wrong rule for ISO; it now checks the kernel's invariant directly against the descriptor table, rather than against the buffer.
The OUT unit test asserted actual_length == 0 — it had locked the bug in, conflating "sends no payload back" with "consumed nothing". It now pins the real invariant, sum(table actual_length) == actual_length.
Gate
usbip-sim: clippy --all-targets -D warnings0, fmt clean, 6/6 tests (a debug build, so the corrected debug_assert is exercised by the tests rather than compiled out).
⚠ Not yet re-tested on hardware. The env var is currently commented out on the .181 test host.
Follow-up worth doing separately
The usbip transport replacing uhid with no fallback is what turned a protocol bug into "no controller at all", and the attach loop retries indefinitely rather than giving up. A bounded retry that degrades to uhid would keep a future transport bug from costing the user their pad. Not attempted here — it is a behavioural change in the pad-creation path and I would not want to write it untested on top of an unverified transport.
Fixes the field failure from turning on `PUNKTFUNK_DUALSENSE_USBIP=1` (#282): the DualSense vanished from the host entirely — unusable in game and absent from Steam's controller list. The usbip path **replaces** uhid, so a failure leaves no pad at all.
## Enumeration was never the problem
The kernel bound **both** drivers to the virtual pad:
```
usb 11-1: 2:0: sticky mixer values (-15360/0/48 => 0), disabling ← snd-usb-audio bound
playstation 0003:054C:0CE6.0011: hidraw6: USB HID v1.11 Gamepad
[Sony Interactive Entertainment DualSense Wireless Controller]
on usb-vhci_hcd.0-1/input3 ← hid-playstation bound
```
So the composite descriptor set, the UAC function and the `054C:0CE6` identity are all accepted — the load-bearing assumption of #282 holds. ~56 ms later, as soon as `snd-usb-audio` submitted the first isochronous OUT URB:
```
usb 11-1: recv xbuf, 0
vhci_hcd: sendmsg failed!, ret=-32 for 48 ← -32 = EPIPE
vhci_hcd vhci_hcd.0: stop threads / release socket / disconnect device
```
## The bug
`usbip_ret_submit_iso` filled each **packet descriptor's** `actual_length` correctly (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`. An OUT reply therefore stated per-packet actuals summing to N × 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**, not the URB. Hence the pad disappearing outright, and the attach loop retrying forever.
The function's own doc comment already stated the required semantics — *"On an OUT endpoint there is no payload to return and the value is the number of bytes we accepted… Reporting the empty reply's length there would tell the kernel the device swallowed nothing"* — only the aggregate missed them.
## The fix
- `actual_length` is now the running total of the per-packet actuals: unchanged for IN (where it already equalled the buffer length) and correct for OUT.
- `to_bytes()`'s `debug_assert` encoded the same wrong rule for ISO; it now checks the kernel's invariant directly against the descriptor table, rather than against the buffer.
- **The OUT unit test asserted `actual_length == 0`** — it had locked the bug in, conflating "sends no payload back" with "consumed nothing". It now pins the real invariant, `sum(table actual_length) == actual_length`.
## Gate
`usbip-sim`: clippy `--all-targets -D warnings` **0**, fmt clean, **6/6** tests (a debug build, so the corrected `debug_assert` is exercised by the tests rather than compiled out).
⚠ **Not yet re-tested on hardware.** The env var is currently commented out on the .181 test host.
## Follow-up worth doing separately
The usbip transport replacing uhid with no fallback is what turned a protocol bug into "no controller at all", and the attach loop retries indefinitely rather than giving up. A bounded retry that degrades to uhid would keep a future transport bug from costing the user their pad. Not attempted here — it is a behavioural change in the pad-creation path and I would not want to write it untested on top of an unverified transport.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the field failure from turning on
PUNKTFUNK_DUALSENSE_USBIP=1(#282): the DualSense vanished from the host entirely — unusable in game and absent from Steam's controller list. The usbip path replaces uhid, so a failure leaves no pad at all.Enumeration was never the problem
The kernel bound both drivers to the virtual pad:
So the composite descriptor set, the UAC function and the
054C:0CE6identity are all accepted — the load-bearing assumption of #282 holds. ~56 ms later, as soon assnd-usb-audiosubmitted the first isochronous OUT URB:The bug
usbip_ret_submit_isofilled each packet descriptor'sactual_lengthcorrectly (bytes accepted — the requested length on OUT), but set the URB-levelactual_lengthfromtransfer_buffer.len(), and the transfer buffer is only populatedinbound. An OUT reply therefore stated per-packet actuals summing to N × 392 against a total of 0.The kernel's
usbip_recv_iso()sums the packet table and compares it to the URB'sactual_length; on a mismatch it raisesERROR_TCPand returns-EPIPE, which drops the connection, not the URB. Hence the pad disappearing outright, and the attach loop retrying forever.The function's own doc comment already stated the required semantics — "On an OUT endpoint there is no payload to return and the value is the number of bytes we accepted… Reporting the empty reply's length there would tell the kernel the device swallowed nothing" — only the aggregate missed them.
The fix
actual_lengthis now the running total of the per-packet actuals: unchanged for IN (where it already equalled the buffer length) and correct for OUT.to_bytes()'sdebug_assertencoded the same wrong rule for ISO; it now checks the kernel's invariant directly against the descriptor table, rather than against the buffer.actual_length == 0— it had locked the bug in, conflating "sends no payload back" with "consumed nothing". It now pins the real invariant,sum(table actual_length) == actual_length.Gate
usbip-sim: clippy--all-targets -D warnings0, fmt clean, 6/6 tests (a debug build, so the correcteddebug_assertis exercised by the tests rather than compiled out).⚠ Not yet re-tested on hardware. The env var is currently commented out on the .181 test host.
Follow-up worth doing separately
The usbip transport replacing uhid with no fallback is what turned a protocol bug into "no controller at all", and the attach loop retries indefinitely rather than giving up. A bounded retry that degrades to uhid would keep a future transport bug from costing the user their pad. Not attempted here — it is a behavioural change in the pad-creation path and I would not want to write it untested on top of an unverified transport.
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.