Never ingest a pad attached through usbip-win2 as input - #31
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ownVirtualSonyPathsregistry 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 fromDS4Devices.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.MoonlightVirtualDevicePolicykeeps 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:
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