fix(connector,session): propagate negotiated share_id to all outgoing ShareDataPdu#1147
Conversation
The share_id from the Server Demand Active PDU was decoded and used correctly for the Client Confirm Active response, but never stored for subsequent use. All other outgoing ShareDataPdu messages (finalization sequence, input events, frame acknowledges) were sent with share_id 0. This is especially problematic after a Deactivation-Reactivation Sequence triggered by a DisplayControl resize, where the server assigns a new share_id. Frame acknowledge PDUs sent with the wrong share_id may be ignored by the server, causing frame delivery to degrade progressively. Store the negotiated share_id in ConnectionActivationState::Finalized, thread it through ConnectionFinalizationSequence, x224::Processor, and FrameMarkerProcessor so all outgoing PDUs use the correct value. Ref: Devolutions#447
9b0d013 to
3a7adbe
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes a protocol conformance bug where share_id was always sent as 0 in all outgoing ShareDataPdu messages. The correct behavior per MS-RDPBCGR is to echo back the share_id assigned by the server in its Demand Active PDU. The most performance-critical fix is the frame acknowledge PDU path, since mismatched share_id values in frame ACKs could cause servers to delay or ignore them, leading to the "frames slow down after resize" regression reported in issue #447.
Changes:
ironrdp-connector:ConnectionFinalizationSequenceandConnectionActivationStatenow carry and propagate the negotiatedshare_id;ConnectionResultexposes it to downstream consumers.ironrdp-session:x224::ProcessorandFrameMarkerProcessorstore theshare_idand use it inencode_static()and frame acknowledge PDUs respectively;ProcessorBuilderacceptsshare_id.- Application-level handlers and FFI: all reactivation handlers in
ironrdp-client,ironrdp-web, the test suite, and the FFI layer now extract and pass through the newshare_idfromConnectionActivationState::Finalized.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
crates/ironrdp-connector/src/connection_activation.rs |
Captures share_id from Server Demand Active; threads it through ConnectionFinalization and Finalized states |
crates/ironrdp-connector/src/connection_finalization.rs |
Stores share_id in ConnectionFinalizationSequence; uses it for all 4 finalization PDUs |
crates/ironrdp-connector/src/connection.rs |
Adds share_id field to ConnectionResult; destructures it from ConnectionActivationState::Finalized |
crates/ironrdp-session/src/x224/mod.rs |
Stores share_id in x224::Processor; uses it in encode_static() |
crates/ironrdp-session/src/fast_path.rs |
Adds share_id to ProcessorBuilder and FrameMarkerProcessor; uses it in frame acknowledge PDUs |
crates/ironrdp-session/src/active_stage.rs |
Passes connection_result.share_id to both x224::Processor and ProcessorBuilder |
crates/ironrdp-client/src/rdp.rs |
Destructures and threads share_id through the reactivation handler |
crates/ironrdp-web/src/session.rs |
Destructures and threads share_id through the reactivation handler |
crates/ironrdp-testsuite-extra/tests/mod.rs |
Destructures and threads share_id through the reactivation test |
ffi/src/connector/activation.rs |
Exposes share_id in ConnectionActivationStateFinalized; correctly ignores it for intermediate ConnectionFinalization state |
ffi/src/connector/result.rs |
Exposes get_share_id() method on FFI ConnectionResult |
ffi/src/session/mod.rs |
Accepts share_id parameter in set_fastpath_processor() |
ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs |
Passes shareId to SetFastpathProcessor() in reactivation handler |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Sounds good! Maybe double check the Copilot comment which sounds suspicious, but otherwise looking good to me!
The x224 processor retained the initial share_id after Deactivation-Reactivation, while only the fast_path processor was rebuilt with the new value. Add set_share_id() to update the x224 processor alongside set_fastpath_processor() in all reactivation handlers.
Hi, During Deactivation-Reactivation, set_fastpath_processor() replaces the fast_path processor with a new one carrying the updated share_id. But x224_processor is never updated. There's no set_x224_share_id() or equivalent. So after reactivation, encode_static() (used for ShutdownRequest, slow-path input, etc.) would send PDUs with the stale share_id. In practice most Windows servers reuse the same share_id across reactivation, so this rarely surfaces, but the spec doesn't guarantee it. The fix is straightforward. I'll add a method to update the x224 processor's share_id and call it alongside set_fastpath_processor() in the reactivation handlers. Thanks, |
Summary
Fixes the
share_idalways being sent as0in all outgoingShareDataPdumessages.This is one of two protocol conformance bugs identified by Vladyslav Nikonov (@pacmancoder) in
#447 (comment) (May 2024),
and is the most likely cause of the progressive frame delivery degradation after resize
originally reported by Isaiah Becker-Mayer (@ibeckermayer) in #447.
Background
Per MS-RDPBCGR 2.2.8.1.1.1.1 (Share Control Header), the
shareIdfield identifiesthe shared session between client and server. The server assigns this value in the
Server Demand Active PDU, and the client must echo it in all subsequent Share Control
and Share Data PDUs.
During the initial connection, IronRDP correctly decodes the
share_idfrom the ServerDemand Active and uses it in the Client Confirm Active response
(
ConnectionActivationSequence::step()atconnection_activation.rs:171). However,the value was never stored beyond that point. Every subsequent outgoing
ShareDataPduused a hardcoded
share_id: 0.This matters most after a Deactivation-Reactivation Sequence (MS-RDPBCGR 1.3.1.3),
which occurs when the server processes a
DISPLAYCONTROL_MONITOR_LAYOUTresize request.The server sends a new Deactivate All followed by a new Demand Active with a potentially
different
share_id. Since the client was always sending0regardless, the mismatchbetween the server's expected
share_idand the client's actualshare_idcould causethe server to ignore or deprioritize frame acknowledges, producing exactly the
"frames slow down after resize" symptom that Isaiah Becker-Mayer (@ibeckermayer) reported.
I was able to reproduce this degradation pattern testing with
encode_resize()fromActiveStageagainst GNOME/Mutter (Fedora 43) and Hyprland (Arch), both using theEGFX V10 AVC420 codec path. See #447 (comment) for details.
What this PR does
Captures the
share_idfrom the Server Demand Active PDU and threads it through everycode path that emits outgoing
ShareDataPdumessages:ironrdp-connector:
ConnectionActivationState::ConnectionFinalizationand::Finalizednow carryshare_idConnectionFinalizationSequencestoresshare_idand uses it for all four finalizationPDUs (Synchronize, ControlCooperate, RequestControl, FontList) instead of
0ConnectionResultexposesshare_idso downstream consumers have access to thenegotiated value
ironrdp-session:
x224::Processorstoresshare_idand uses it inencode_static()(input events,shutdown request) instead of
0FrameMarkerProcessorstoresshare_idand uses it in frame acknowledge PDUs insteadof
0. This is the most performance-critical fix: frame acks are the server's primaryflow control signal for EGFX frame delivery.
ProcessorBuilderacceptsshare_idso it can be set both at initial connection andafter reactivation
Application-level reactivation handlers:
ironrdp-client/src/rdp.rs,ironrdp-web/src/session.rs, andironrdp-testsuite-extra/tests/mod.rsall destructure the newshare_idfield fromConnectionActivationState::Finalizedand pass it through when rebuilding theFastPathProcessorafter reactivationFFI bindings:
ConnectionResult::get_share_id()addedConnectionActivationStateFinalizedcarries and exposesshare_idActiveStage::set_fastpath_processor()acceptsshare_idparameterAffected callsites
Seven callsites were sending hardcoded
share_id: 0:connection_finalization.rs:101self.share_idconnection_finalization.rs:118self.share_idconnection_finalization.rs:135self.share_idconnection_finalization.rs:145self.share_idx224/mod.rs:200self.share_idfast_path.rs:630self.share_idconnection_activation.rs:171Verification
cargo xtask check fmt -vpassescargo xtask check lints -vpassescargo xtask check tests -vpasses (all existing tests; wire format is unchanged,only the value placed in the
share_idfield changes)Related
ShareDataHeader::uncompressedLengthcalculation(the second protocol bug Vladyslav Nikonov (@pacmancoder) identified in the same comment)
max_unacknowledged_frame_countfrom 20 back to 2(the
FIXME(#447)workaround inconnection_activation.rs) once this fix is validated