Skip to content
Open
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
13 changes: 13 additions & 0 deletions crates/ironrdp-connector/src/connection_activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use crate::{
MonotonicInstant, Sequence, State, Written, general_err, reason_err,
};

const MAX_DESKTOP_DIM: u16 = 32766;

/// Represents the Capability Exchange and Connection Finalization phases
/// of the connection sequence (section [1.3.1.1]).
///
Expand Down Expand Up @@ -292,6 +294,17 @@ impl Sequence for ConnectionActivationSequence {
height: self.config.desktop_size.height,
});

if !(1..=MAX_DESKTOP_DIM).contains(&desktop_size.width)
|| !(1..=MAX_DESKTOP_DIM).contains(&desktop_size.height)
Comment thread
mmorrissette-devolutions marked this conversation as resolved.
Comment thread
mmorrissette-devolutions marked this conversation as resolved.
{
Comment thread
mmorrissette-devolutions marked this conversation as resolved.
return Err(reason_err!(
"ConnectionActivation::CapabilitiesExchange",
"server desktop size {}x{} is outside the supported range",
desktop_size.width,
desktop_size.height
));
}
Comment on lines +297 to +306

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.

[skeptical] Desktop size range check also gates the client's own configuration and the error blames the server — low 🟡 — The negotiated desktop_size falls back to self.config.desktop_size when the server sends no Bitmap capability set (existing lines 283-295), so the added 1..=32766 check also rejects out-of-range client-side configuration, a case the PR does not describe, and the message 'server desktop size {}x{} is outside the supported range' incorrectly attributes a client-supplied value to the server. Impact is narrow: it needs a non-compliant server that omits the Bitmap capability plus a misconfigured client, and failing fast on a zero-size config improves on the previous empty-framebuffer behavior. Restricting the check to the server-provided Bitmap size or adjusting the message for the fallback case would resolve it.

Comment on lines +297 to +306

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] Client drops connections on server desktop sizes above 32766, a bound not required by MS-RDPBCGR — medium 🟠 — The new check aborts Capabilities Exchange when the Demand Active bitmap capability advertises width or height of 32767..65535, but MS-RDPBCGR defines TS_BITMAP_CAPABILITYSET.desktopWidth/desktopHeight as unconstrained u16 and its client-side Demand Active processing rules contain no desktop-size range validation. The 32766 limit is a client convention backed by real server behavior (ERRINFO_VIRTUALDESKTOPTOOLARGE, xrdp 0x7FFE, ironrdp-acceptor's 8192 clamp) and documented by the PR, yet it remains a stricter-than-specified interop restriction that can terminate connections with spec-conformant third-party servers advertising large desktops. Zero-dimension rejection is a defensible policy; the upper bound deserves explicit documentation as a product limit.

Comment on lines +297 to +306

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.

[skeptical] Range check also validates the client's own config size but reports it as a server error — low 🟡 — When the Demand Active carries no Bitmap capability set, desktop_size falls back to self.config.desktop_size, and the new check then rejects the client's own requested size. Config::desktop_size has no documented constraint and is sent unvalidated in TS_UD_CS_CORE, so a caller configuring e.g. 65535x65535 now fails at activation with 'server desktop size ... is outside the supported range' even though the server never announced that value. The rejection is defensible (the server could not honor such a request anyway), but the error misattributes the value to the server and surfaces late; validate config at construction or broaden the message.

Comment on lines +297 to +306

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.

[skeptical] Native clients still abort on a valid maximum desktop size — low 🟡 — With the new bound, a conforming server can still announce 32766x32766 (~4.3 GB at 32bpp). Native clients (ironrdp-client, FFI) keep the infallible DecodedImage::new, which performs unchecked multiplication and vec![0; len], so a memory-constrained host still aborts on a fully valid announcement; only the out-of-spec/overflowing cases are eliminated. The PR body acknowledges this and defers a fallible DecodedImage::new to a follow-up, which is reasonable scope isolation, but the original failure mode is narrowed rather than closed for native clients and the follow-up should be tracked.


let share_id = share_control_ctx.share_id;

let client_confirm_active = rdp::headers::ShareControlPdu::ClientConfirmActive(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,40 @@ fn demand_active_static_channel_chunk_size(chunk_size: Option<u32>) -> usize {
}
}

fn step_demand_active_with_desktop_size(width: u16, height: u16) -> ironrdp_connector::ConnectorResult<Written> {
let mut demand_active = SERVER_DEMAND_ACTIVE.clone();
let bitmap = demand_active
.pdu
.capability_sets
.iter_mut()
.find_map(|capability_set| match capability_set {
CapabilitySet::Bitmap(bitmap) => Some(bitmap),
_ => None,
})
.expect("server demand active should include a bitmap capability");
bitmap.desktop_width = width;
bitmap.desktop_height = height;

let mut sequence = ConnectionActivationSequence::new(test_config(), IO_CHANNEL_ID, USER_CHANNEL_ID);
let mut output = WriteBuf::new();
let frame = encode_server_share_control(ShareControlPdu::ServerDemandActive(demand_active));
sequence.step(&frame, None, &mut output)
}
Comment on lines +125 to +143

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.

[code-compressor] New test helper repeats existing demand-active step scaffolding — low 🟡 — step_demand_active_with_desktop_size repeats the scaffolding of the pre-existing demand_active_static_channel_chunk_size (96-123): clone SERVER_DEMAND_ACTIVE, find_map one capability set out of the PDU, mutate it, build a ConnectionActivationSequence with the same test_config()/channel IDs, encode via encode_server_share_control, and sequence.step(...). A single closure-parameterized helper (fn step_demand_active_with(mutate: impl FnOnce(&mut DemandActivePdu))) would replace both, removing roughly eight duplicated lines and a second divergent copy of the sequence-bootstrapping recipe without changing test behavior. Tradeoff: the file's existing convention is inline scaffolding (a third variant at 342-364 repeats parts of it), so this is an optional consolidation with no runtime impact.


#[test]
fn demand_active_accepts_server_desktop_size_within_range() {
for (width, height) in [(1, 1), (1920, 1080), (32766, 32766)] {
assert!(step_demand_active_with_desktop_size(width, height).is_ok());
}
}

#[test]
fn demand_active_rejects_server_desktop_size_out_of_range() {
for (width, height) in [(0, 768), (1024, 0), (32767, 768), (1024, 32767), (u16::MAX, u16::MAX)] {
assert!(step_demand_active_with_desktop_size(width, height).is_err());
}
}

#[test]
fn demand_active_uses_valid_server_static_channel_chunk_size() {
for chunk_size in [
Expand Down
19 changes: 19 additions & 0 deletions crates/ironrdp-web/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,11 @@ impl iron_remote_desktop::Session for Session {
let desktop_height =
NonZeroU32::new(u32::from(connection_result.desktop_size.height)).context("desktop height is zero")?;

ensure_framebuffer_allocatable(
connection_result.desktop_size.width,
connection_result.desktop_size.height,
)?;

let mut gui =
Canvas::new(self.render_canvas.clone(), desktop_width, desktop_height).context("canvas initialization")?;

Expand Down Expand Up @@ -1073,6 +1078,7 @@ impl iron_remote_desktop::Session for Session {
} = connection_activation.connection_activation_state()
{
debug!("Deactivation-Reactivation Sequence completed");
ensure_framebuffer_allocatable(desktop_size.width, desktop_size.height)?;

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.

[skeptical] Allocation probe runs while the old framebuffer is still alive, so resize can still abort on wasm32 — medium 🟠 — In the Deactivation-Reactivation path, ensure_framebuffer_allocatable(desktop_size.width, desktop_size.height) runs immediately before image = DecodedImage::new(...), which allocates the new framebuffer while the previous image is still held (dropped only after the assignment). The probe checks the new size in isolation, so on wasm32 a resize between two large valid desktops can still exceed linear memory and abort, precisely in the scenario the probe was added for. The probe is a heuristic; the complete fix is a fallible DecodedImage::new, deferred by the author. The probe should account for the live framebuffer or this residual window should be documented.

image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height);
if !active_stage.reactivate(
connection_activation.io_channel_id(),
Expand Down Expand Up @@ -1920,6 +1926,19 @@ where
}
}

fn ensure_framebuffer_allocatable(width: u16, height: u16) -> anyhow::Result<()> {
let len = usize::from(width)
.checked_mul(usize::from(height))
.and_then(|pixels| pixels.checked_mul(usize::from(PixelFormat::RgbA32.bytes_per_pixel())))
.with_context(|| format!("framebuffer size overflows for a {width}x{height} desktop"))?;

Vec::<u8>::new()
.try_reserve_exact(len)
.with_context(|| format!("not enough memory for a {width}x{height} desktop framebuffer"))?;

Ok(())
}
Comment on lines +1929 to +1940

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.

[skeptical] Framebuffer allocation probe does not guarantee the subsequent infallible allocation succeeds — medium 🟠 — ensure_framebuffer_allocatable validates a single W*H*4 reservation via try_reserve_exact and immediately frees it, but the infallible vec![0; len] in DecodedImage::new (ironrdp-session/src/image.rs:147-151) runs while other large buffers are live: Canvas::new allocates the canvas backing store between the probe and the image on the initial path (673-689), and in the Deactivation-Reactivation path (1081-1082) the old image is only dropped after the new one is constructed, so peak usage is roughly twice what the probe validated. Under memory pressure the wasm32 abort path remains reachable, notably on resolution change with a large previous framebuffer resident. The helper also duplicates DecodedImage::new's size formula and will drift if the layout changes. The probe is an acceptable stopgap, but the residual abort risk should be documented; a fallible DecodedImage constructor or reset would close it fully.

Comment on lines +1929 to +1940

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.

[code-compressor] ensure_framebuffer_allocatable can take connector::DesktopSize instead of two u16s — low 🟡 — Both call sites (session.rs:673-676 and :1081) pass width and height extracted from a connector::DesktopSize already in scope. Changing the signature to accept &connector::DesktopSize (a type already referenced in this file) removes field duplication at both call sites and ~3 lines with no behavior change. The checked_mul chain should be kept as-is: removing it would hard-code the connector's 1..=32766 invariant into ironrdp-web. Cosmetic, low-impact simplification.


#[expect(clippy::as_conversions, clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn f64_to_u16_saturating_cast(value: f64) -> u16 {
value as u16
Expand Down
Loading