Skip to content

feat(rdpeusb): add the usb translation module - #1683

Open
uchouT (uchouT) wants to merge 3 commits into
Devolutions:masterfrom
uchouT:ironrdp-rdpeusb-usb
Open

feat(rdpeusb): add the usb translation module#1683
uchouT (uchouT) wants to merge 3 commits into
Devolutions:masterfrom
uchouT:ironrdp-rdpeusb-usb

Conversation

@uchouT

@uchouT uchouT (uchouT) commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Part of #1516, also see: #1417

What this adds

RDPEUSB carries USB operations as TS_URB structures with Windows USBD conventions: URB function codes, USBD transfer flags, pipe and configuration handles, and USBD status values. A caller that already speaks USB in protocol-independent terms should not have to assemble those by hand.

More to the point, assembling them by hand is where things go quietly wrong. The TS_URB payload, the URB function code, the transfer envelope, the flags, and the buffer shape all have to agree with each other, and nothing in the type system says so.

The new usb module maps ironrdp-usb requests onto complete backend-facing RDPEUSB packets, so those five cannot disagree, and maps completions back:

  • descriptor, configuration, interface, status, feature, and vendor/class control requests;
  • bulk, interrupt, and isochronous transfers, in both directions;
  • configuration and interface selection, including the pipe information the client returns;
  • pipe reset, device reset, and current frame number;
  • USBD status to UsbError on the completion path.

The module is sans-I/O. It does not allocate request IDs, track device state, or manage pending-request lifetimes — those belong to the layer above, in #1417.

Why this lives in ironrdp-rdpeusb

Choosing TS_URB forms and applying Windows USBD conventions is RDPEUSB knowledge, so the adapter belongs here. Its inputs stay protocol-independent, which is exactly what lets #1417 expose a device facade expressed in plain USB terms.

Also in this PR

  • CompletionData — the channel delivers four distinct completion result types, and a caller correlating completions against its own pending requests needs to name them uniformly. This enum lets a single type carry any RDPEUSB completion payload.
  • BcdVersion reuseio::device::UsbBcdVersion is removed in favour of ironrdp_usb::BcdVersion, which is the same type with the same is_valid() semantics. ironrdp-rdpeusb is publish = false, so this is not a break in any released API.

Tests are in ironrdp-testsuite-core (tests/rdpeusb/usb.rs) and cover the translation contract: which URB form each request maps to, the flag and handle values carried, and how completions are validated and unpacked.

Copilot AI balanced review requested due to automatic review settings August 16, 2026 22:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added maintainer-required Maintainer review or intervention is required risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny scope/core Touches the core architectural tier size/XXL Size: 1300 or more counted lines or 50 or more files labels Aug 16, 2026
The channel delivers four distinct completion result types, and a caller
correlating completions with its own pending requests has to name them
uniformly to store or dispatch them.

Add `CompletionData`, an enum over the I/O control, internal I/O control,
and transfer in/out completion results, so a single type can carry any
RDPEUSB completion payload.
…EUSB requests

RDPEUSB carries USB operations as TS_URB structures with Windows USBD
conventions: URB function codes, USBD transfer flags, pipe and config
handles, and USBD status values. A caller that already speaks USB in
protocol-independent terms should not have to assemble those by hand, and
assembling them by hand is where the TS_URB payload, URB function,
transfer envelope, flags, and buffer shape can silently disagree.

Add an `usb` module that maps `ironrdp-usb` requests onto complete
backend-facing RDPEUSB packets, and maps completions back:

- descriptor, configuration, interface, status, feature, and
  vendor/class control requests;
- bulk, interrupt, and isochronous transfers, in both directions;
- configuration and interface selection, including the pipe information
  the client returns;
- pipe reset, device reset, and current frame number;
- USBD status to `UsbError` mapping on the completion path.

The module is sans-I/O and does not allocate request IDs, track device
state, or manage pending-request lifetimes. It lives in `ironrdp-rdpeusb`
rather than `ironrdp-usb` because selecting TS_URB forms is RDPEUSB
knowledge, while its inputs stay protocol-independent.

Depend on `ironrdp-usb` and cover the translation with tests in
`ironrdp-testsuite-core`.
`UsbBcdVersion` wraps a raw `bcdUSB` value and preserves it without
validating its digits. That is a plain USB concept: nothing about it is
explained by RDPEUSB, and `ironrdp-usb` now models it as `BcdVersion`
so that descriptor accessors can return it directly.

Drop the RDPEUSB copy and use the shared type. The two behaviours that
really are RDPEUSB-specific stay here as module-private helpers: mapping
`bcdUSB` onto the `SupportedUsbVer` capability field, which models only
USB 1.0, 1.1, and 2.0, and the USB 2.0 threshold used to guess a device
speed the backend did not report.
@CBenoit Benoît Cortier (CBenoit) added the ai-review/allow-oversized Allows normal automated review of an oversized pull request label Aug 18, 2026
@github-actions github-actions Bot added breaking-change Includes a breaking change, and requires special scrutiny at the boundaries kind/protocol Affects RDP or related protocol behavior risk/medium Behavioral change that does not substantially alter a core public API ai-reviewed/1 One automated review completed and removed risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny maintainer-required Maintainer review or intervention is required labels Aug 18, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a new sans-I/O `usb` module to ironrdp-rdpeusb translating protocol-independent ironrdp-usb requests into complete RDPEUSB TS_URB packets and back, plus a small, explicitly-justified bundled refactor (BcdVersion reuse, CompletionData enum). The module is self-contained, extensively tested, has no unwrap/panic paths, and matches MS-RDPEUSB wire conventions everywhere I independently checked against the diff. The two bundled changes (dropping the crate-local UsbBcdVersion for ironrdp_usb::BcdVersion, and adding CompletionData) are both directly consumed by the new module and explicitly called out with rationale in the PR description, so they don't read as unrelated scope. The only issues found are a real, narrow test-coverage gap and one design question about failure-payload handling consistency; neither undermines correctness of the shipped code.

Protocol analysis: accepted — Independently spot-checked the highest-risk mappings (control-transfer dispatch/fallback, feature/status/descriptor URB function selection, bulk/isochronous direction and flag handling, select-configuration/interface pipe-info derivation, completion status mapping, and the bcdUSB-to-SupportedUsbVer refactor) against the diff and found no contradictions with the handoff's conformance assessments. The handoff's own uncertainty notes (WDK constant values, request-side ISO packet descriptor semantics) are reasonable given those values originate outside the local MS-RDPEUSB corpus. The handoff's flagged test gaps for control_transfer()'s canonical GET_CONFIGURATION/GET_INTERFACE/SET_DESCRIPTOR dispatch paths are confirmed real by inspection of the added test file.

};
let TsUrbInKind::CtlTransfer(urb) = request.ts_urb.kind else {
panic!("noncanonical request lost fields in a typed TS_URB");
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non_blocking / low: The only tests exercising control_transfer() with GET_CONFIGURATION/GET_INTERFACE/CLEAR_FEATURE setup packets deliberately use non-canonical field values so the noncanonical fallback path is hit (asserting they fall back to the generic TsUrbInKind::CtlTransfer). A separate test (configuration_and_interface_queries_use_typed_urbs, line 510) exercises the typed get_configuration()/get_interface() builder functions directly, but nothing calls control_transfer() with a canonical GET_CONFIGURATION/GET_INTERFACE (or the standard SET_DESCRIPTOR) setup packet to confirm control_transfer() itself reaches the typed branch in standard_control_transfer rather than always falling through to the generic URB. A latent guard-condition typo in one of those match arms (e.g. wrong recipient/value/index/length comparison) would not be caught by the current suite.

let Some(func) = get_status_function(recipient) else {
return Err(data);
};
Ok(TransferRequest::In(transfer_in(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question / low: isochronous_completion() special-cases a failed URB by returning the USB error before requiring the iso_packet list to match the requested packet count, with a comment noting real clients (e.g. FreeRDP) can return an empty packet list on overall transfer failure. select_configuration_completion, select_interface_completion, and current_frame_number_completion have no equivalent allowance: they require the TS_URB_*_RESULT payload variant to be present via ensure_empty_output/pattern match regardless of whether the completion's HRESULT/USBD status indicates failure. If any real client behaves the same way for these completions (omitting the result substructure on failure) as it does for isochronous transfers, translate would hard-fail with a CompletionError on an otherwise valid failed URB completion instead of surfacing the USB error to the caller. Is there evidence (from a reference client) that these payloads are guaranteed present on failure, unlike the isochronous case?

@github-actions github-actions Bot added the maintainer-required Maintainer review or intervention is required label Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review/allow-oversized Allows normal automated review of an oversized pull request ai-reviewed/1 One automated review completed breaking-change Includes a breaking change, and requires special scrutiny at the boundaries kind/protocol Affects RDP or related protocol behavior maintainer-required Maintainer review or intervention is required risk/medium Behavioral change that does not substantially alter a core public API scope/core Touches the core architectural tier size/XXL Size: 1300 or more counted lines or 50 or more files

Development

Successfully merging this pull request may close these issues.

3 participants