Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions crates/ironrdp-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2033,7 +2033,7 @@ impl RdpServer {
/// on, a preemption winner is indistinguishable from a normally-accepted
/// connection.
async fn serve_negotiated(&mut self, candidate: Box<NegotiatedCandidate>) -> ServerResult<()> {
self.display_suppressed.store(false, Ordering::Relaxed);
self.reset_connection_state();

let mut candidate = candidate;
// Only NOW build the channel backends: this connection has
Expand Down Expand Up @@ -2155,20 +2155,37 @@ impl RdpServer {
result
}

/// Per-connection state must start fresh for every client, on both the
/// normal path and a preemption winner.
fn reset_connection_state(&mut self) {
// If the previous client disconnected while it had sent
// `SuppressOutput { None }` (e.g., closed the mstsc window while
// minimized so the matching resume PDU never arrived), the flag would
// still read `true` here and the display backend would silently drop
// frames for the entire new session until/unless the new client
// happens to send a `RefreshRectangle` or `SuppressOutput { Some(rect) }`.
// Resetting here also covers backends that share an externally-created
// Arc via `set_display_suppressed_handle()`.
self.display_suppressed.store(false, Ordering::Relaxed);

// Network characteristics describe one client's path. Kept across
// connections, the previous client's round trips would hold down this
// one's baseRTT (the lowest RTT seen, MS-RDPBCGR 2.2.14.1.5), its
// figures would be reported until overwritten, and a measurement it
// left pending would hold up the next one.
if self.autodetect.is_some() {
self.autodetect = Some(AutoDetectManager::new());
}
self.autodetect_rtt.store(u32::MAX, Ordering::Relaxed);
self.autodetect_baseline_rtt.store(u32::MAX, Ordering::Relaxed);
self.autodetect_bandwidth.store(u32::MAX, Ordering::Relaxed);
Comment on lines +2179 to +2181

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.

[protocol] Bandwidth-handle reset is invisible to embedders watching the generation counter — low 🟡 — reset_connection_state stores u32::MAX into autodetect_rtt/autodetect_baseline_rtt/autodetect_bandwidth with Relaxed and, unlike every measurement mutation (server.rs:3975, 3989), performs no paired autodetect_bandwidth_generation increment. Not incrementing is correct per the counter's documented meaning ('increments every time a Bandwidth Measure transaction completes', lines 764-766) and the PR body's stated rationale. The residual issue is that the store has no release at all, so an embedder relying on the documented Release/Acquire pattern to observe bandwidth changes will never learn that the previous connection's figure was cleared for the new connection. Wire behavior is unaffected: the fresh AutoDetectManager ensures Network Characteristics Results are correct (MS-RDPBCGR 2.2.14.1.5). An embedder-facing doc note on the reset, or an Acquire-visible signal, would close the gap.

}

async fn run_connection_inner<S>(&mut self, stream: S, tls: TransportTls) -> ServerResult<()>
where
S: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
// Per-connection state must start fresh: if the previous client
// disconnected while it had sent `SuppressOutput { None }` (e.g.,
// closed the mstsc window while minimized so the matching resume
// PDU never arrived), the flag would still read `true` here and the
// display backend would silently drop frames for the entire new
// session until/unless the new client happens to send a
// `RefreshRectangle` or `SuppressOutput { Some(rect) }`. Resetting
// here also covers backends that share an externally-created Arc via
// `set_display_suppressed_handle()`.
self.display_suppressed.store(false, Ordering::Relaxed);
self.reset_connection_state();

let size = self.display.lock().await.size().await;
let monitor_count = self.display.lock().await.monitor_count().await;
Expand Down
35 changes: 35 additions & 0 deletions crates/ironrdp-testsuite-core/tests/server/autodetect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,41 @@ fn with_autodetect_bandwidth_generation_handle_round_trips_the_same_arc() {
);
}

/// A new connection does not inherit the previous one's network figures: the
/// handles read "not measured" again until this client has been measured.
#[tokio::test]
async fn a_new_connection_starts_with_no_measurements() {
use core::net::{Ipv4Addr, SocketAddr};
use core::sync::atomic::Ordering;

use ironrdp_server::RdpServer;

let mut server = RdpServer::builder()
.with_addr(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.with_no_security()
.with_no_input()
.with_no_display()
.build();
server.enable_autodetect();

// What an earlier connection left behind.
let rtt = server.autodetect_rtt_handle();
let baseline_rtt = server.autodetect_baseline_rtt_handle();
let bandwidth = server.autodetect_bandwidth_handle();
rtt.store(12, Ordering::Relaxed);
baseline_rtt.store(3, Ordering::Relaxed);
bandwidth.store(80_000, Ordering::Relaxed);

// A client that vanishes straight away still starts a connection.
let (client, server_side) = tokio::io::duplex(64);
drop(client);
let _ = server.run_connection(server_side).await;

assert_eq!(rtt.load(Ordering::Relaxed), u32::MAX);
assert_eq!(baseline_rtt.load(Ordering::Relaxed), u32::MAX);
assert_eq!(bandwidth.load(Ordering::Relaxed), u32::MAX);
}

#[test]
fn stale_probe_expiry() {
let mut mgr = AutoDetectManager::new();
Expand Down
Loading