Skip to content

Never ingest a pad attached through usbip-win2 as input - #31

Merged
potpiemuncher merged 1 commit into
mainfrom
fix/usbip-attached-pads-are-not-input
Aug 1, 2026
Merged

Never ingest a pad attached through usbip-win2 as input#31
potpiemuncher merged 1 commit into
mainfrom
fix/usbip-attached-pads-are-not-input

Conversation

@potpiemuncher

Copy link
Copy Markdown
Owner

Closes the residual #30 deliberately left open, at the layer it always belonged.

The gap

Input discovery's protection against re-ingesting our own virtual output was a memory: the in-process ownVirtualSonyPaths registry plus an active-port check — both records of what this session created. A session that dies hard takes the records with it, so its leftover pad was a perfectly ordinary DualSense to the next start: ingested, mapped to an output, recursed on. The old startup port sweep prevented that by detaching the leftover — and #30 established that detach-on-heuristic disconnects other applications' live pads, so the sweep now observes only, leaving the ingestion path with nothing.

The same gap covered a case nobody had named: another application's live virtual pad (the native-mode DS4Windows serving a DualSense) was equally ingestible the moment its device type was enabled.

The rule

UsbipAttachedInputPolicy, consulted from DS4Devices.IsRealDS4: any pad whose devnode ancestry reaches the usbip-win2 UDE controller is a virtual output being served by something — ours, a dead session's, or another program's — and none of those is input. Ancestry does not depend on who remembers creating the device. The walk already existed (Global.CheckIfUsbIpWin2Device); the change is consulting it for every candidate rather than only for this session's registered ports.

  • Own live outputs: rejected quietly, as before.
  • Unmanaged usbip pads: rejected with one log line per path, naming both readings (leftover / another program's live controller) and pointing at the backend-process card that can clear a leftover with consent.
  • Moonlight/ViGEm virtual pads: untouched — this policy is scoped to the usbip-win2 controller, and MoonlightVirtualDevicePolicy keeps its own admission rules.

Deliberately given up: a real controller forwarded from another machine over usbip-win2 can no longer be input. usbip-win2 exists on these machines solely as VIIPER's transport, remote forwarding has VirtualHere as the supported route (see CheckIfVirtualDevice's exclusion list), and the refusal is named in the log rather than silent.

Verification

Suite: 867 passed / 0 failed (CI filter), from 862. Five policy tests: the verdict table, warn-once per path across hotplug re-runs, and the announcement's required content (evidence, both readings, remedy).

Live, against the real coexistence scenario. With the native-mode DS4Windows serving its virtual DualSense on usbip port 1 and DualSense input enabled in an isolated run, one discovery pass produced both halves of the proof:

Ignoring a controller attached through usbip-win2 (\?\hid#vid_054c&pid_0ce6&mi_03#...):
pads on that controller are virtual outputs being served by some application - possibly
Thrum's own from a session that ended abruptly, possibly another program's live controller -
and are never used as input. ...
Found Controller: D0:BC:C1:3F:18:90 (BT) (DualSense).

The usbip-attached virtual pad refused; the physical Bluetooth DualSense — same VID/PID — accepted in the same breath. Position in the device tree is the discriminator, not controller identity. The native application, its pad, and its import were untouched throughout.

🤖 Generated with Claude Code

Input discovery's protection against re-ingesting our own virtual output was
a memory: an in-process path registry and an active-port check, both records
of what this session created. A session that dies hard takes the records with
it, so its leftover pad was a perfectly ordinary DualSense to the next start
-- ingested, mapped to an output, and recursed on. The old startup port sweep
prevented that by detaching the leftover, and stopped doing so when it turned
out to be detaching other applications' live pads; this closes the residual
that removal left, at the layer it always belonged.

The rule is positional: any pad whose devnode ancestry reaches the usbip-win2
UDE controller is a virtual output being served by something -- this
session's own, a dead session's leftover, or another program's live
controller -- and none of those is input. Ancestry does not depend on who
remembers creating the device. The walk already existed
(Global.CheckIfUsbIpWin2Device, built for the active-port check); the change
is consulting it for every candidate rather than only for ports this session
registered.

Our own live outputs are rejected quietly, as before. An unmanaged usbip pad
is rejected with one line per path, because nothing else will ever explain to
the user why a pad they can see is being ignored: the line names both
readings and points at the backend-process card that can clear a leftover
with consent.

Deliberately given up: a real controller forwarded from another machine over
usbip-win2 can no longer be input. On these machines usbip-win2 exists solely
as VIIPER's transport, remote forwarding has VirtualHere as the supported
route, and the refusal is named in the log rather than silent.

Five policy tests, and the live coexistence scenario as the proof: with the
native-mode DS4Windows serving its virtual DualSense and DualSense input
enabled, one discovery pass refused the usbip-attached pad and accepted the
physical Bluetooth DualSense -- same VID and PID -- in the same breath.
Position in the device tree is the discriminator, not controller identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@potpiemuncher
potpiemuncher merged commit 1450b9c into main Aug 1, 2026
4 checks passed
@potpiemuncher
potpiemuncher deleted the fix/usbip-attached-pads-are-not-input branch August 1, 2026 04:49
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