Summary
In ironrdp-server, a mouse button event delivered to RdpServerInputHandler::mouse carries no position, even though the wire PDU always does. As a result, a click lands wherever the server's cursor happened to be — not where the client actually clicked — for any client that sends a button PDU without a preceding move PDU.
Most clients (mstsc, Microsoft Remote Desktop / the desktop Windows App) send a separate MOVE PDU immediately before the button, so their clicks already land correctly and the bug is masked. It bites clients that send a tap as a single button PDU. A concrete repro is below.
Root cause
The MouseEvent variants for buttons are position-less unit variants — only Move/Scroll/RelMove carry coordinates:
crates/ironrdp-server/src/handler.rs:27
pub enum MouseEvent {
Move { x: u16, y: u16 },
RightPressed, RightReleased,
LeftPressed, LeftReleased,
MiddlePressed, MiddleReleased,
Button4Pressed, Button4Released,
Button5Pressed, Button5Released,
VerticalScroll { value: i16 },
Scroll { x: i32, y: i32 },
RelMove { x: i32, y: i32 },
}
The From<MousePdu> / From<MouseXPdu> conversions therefore discard the PDU's x_position/y_position whenever a button flag is set — the coordinates survive only when the PDU degenerates to a Move:
crates/ironrdp-server/src/handler.rs:149 (From<MousePdu>) and :176 (From<MouseXPdu>) both do
if flags.contains(LEFT_BUTTON) { /* -> LeftPressed / LeftReleased, x/y dropped */ }
else if flags.contains(RIGHT_BUTTON) { /* -> Right..., x/y dropped */ }
...
else { MouseEvent::Move { x, y } } // position only survives here
The dispatch then forwards that position-less event straight through — every mouse arm calls handler.mouse(mouse.into()):
crates/ironrdp-server/src/server.rs (fast-path :1548/:1552/:1556, slow-path :1708/:1712).
So the position a button PDU carried is lost before the handler ever sees it, on both the regular (MousePdu) and extended (MouseXPdu) absolute-position paths.
Reproduction
- Client: Windows App on iOS/iPadOS, touch mode.
- A tap is sent as a single button PDU (button DOWN then UP at the tap coordinates) with no preceding
MOVE PDU.
- Result: the click registers at the previous cursor position, not where the user tapped — taps feel "off" / "not working."
- A mouse-driven client (mstsc, desktop Windows App) is unaffected because it emits
MOVE → button, so the position is already current when the button arrives.
Impact
Any ironrdp-server consumer serving a client that sends button-without-preceding-move gets mis-placed clicks. It's silent (no error) and easy to misattribute to coordinate scaling.
Suggested fixes
Two shapes, and I think the choice is a maintainer call because one touches the public API:
-
Minimal / non-breaking — synthesize a Move before the button at the point of conversion/dispatch (apply the PDU's x_position/y_position as a Move, then the button). No public-API change. Downside: it has to be repeated at each dispatch arm (or each From), it emits a redundant Move for the common move-then-click client, and the "which flags are position-less" knowledge ends up duplicated alongside the From impls.
-
Root-cause / breaking — give the button events a position. e.g. a single MouseEvent::Button { x, y, button, pressed }, or coordinate fields on the existing pressed/released variants. This fixes MousePdu, MouseXPdu (and structurally the MouseRelPdu button branches) uniformly in one place, with no synthetic event and no duplicated flag knowledge. The cost is a breaking change to the public MouseEvent enum / RdpServerInputHandler contract, which affects downstream server implementations — hence flagging it rather than sending a patch.
Downstream reference
Observed and worked around in a downstream ironrdp-server consumer (macrdp) via option (1) — a small helper that prepends the Move on the regular MousePdu path — which confirms the fix but also surfaced why (2) is cleaner (the same gap is reachable through MousePdu, MouseXPdu, and the MouseRel button branches, so (1) is a per-arm band-aid). Happy to open a PR in whichever shape you prefer.
Environment
Summary
In
ironrdp-server, a mouse button event delivered toRdpServerInputHandler::mousecarries no position, even though the wire PDU always does. As a result, a click lands wherever the server's cursor happened to be — not where the client actually clicked — for any client that sends a button PDU without a preceding move PDU.Most clients (mstsc, Microsoft Remote Desktop / the desktop Windows App) send a separate
MOVEPDU immediately before the button, so their clicks already land correctly and the bug is masked. It bites clients that send a tap as a single button PDU. A concrete repro is below.Root cause
The
MouseEventvariants for buttons are position-less unit variants — onlyMove/Scroll/RelMovecarry coordinates:crates/ironrdp-server/src/handler.rs:27The
From<MousePdu>/From<MouseXPdu>conversions therefore discard the PDU'sx_position/y_positionwhenever a button flag is set — the coordinates survive only when the PDU degenerates to aMove:crates/ironrdp-server/src/handler.rs:149(From<MousePdu>) and:176(From<MouseXPdu>) both doThe dispatch then forwards that position-less event straight through — every mouse arm calls
handler.mouse(mouse.into()):crates/ironrdp-server/src/server.rs(fast-path:1548/:1552/:1556, slow-path:1708/:1712).So the position a button PDU carried is lost before the handler ever sees it, on both the regular (
MousePdu) and extended (MouseXPdu) absolute-position paths.Reproduction
MOVEPDU.MOVE→ button, so the position is already current when the button arrives.Impact
Any
ironrdp-serverconsumer serving a client that sends button-without-preceding-move gets mis-placed clicks. It's silent (no error) and easy to misattribute to coordinate scaling.Suggested fixes
Two shapes, and I think the choice is a maintainer call because one touches the public API:
Minimal / non-breaking — synthesize a
Movebefore the button at the point of conversion/dispatch (apply the PDU'sx_position/y_positionas aMove, then the button). No public-API change. Downside: it has to be repeated at each dispatch arm (or eachFrom), it emits a redundantMovefor the common move-then-click client, and the "which flags are position-less" knowledge ends up duplicated alongside theFromimpls.Root-cause / breaking — give the button events a position. e.g. a single
MouseEvent::Button { x, y, button, pressed }, or coordinate fields on the existing pressed/released variants. This fixesMousePdu,MouseXPdu(and structurally theMouseRelPdubutton branches) uniformly in one place, with no synthetic event and no duplicated flag knowledge. The cost is a breaking change to the publicMouseEventenum /RdpServerInputHandlercontract, which affects downstream server implementations — hence flagging it rather than sending a patch.Downstream reference
Observed and worked around in a downstream
ironrdp-serverconsumer (macrdp) via option (1) — a small helper that prepends theMoveon the regularMousePdupath — which confirms the fix but also surfaced why (2) is cleaner (the same gap is reachable throughMousePdu,MouseXPdu, and theMouseRelbutton branches, so (1) is a per-arm band-aid). Happy to open a PR in whichever shape you prefer.Environment
master@079b4842(verified the code above against it).A-native-client); this is the server dropping an incoming button PDU's position.