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
51 changes: 46 additions & 5 deletions crates/ironrdp-pdu/src/rdp/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,15 @@ impl<'de> Decode<'de> for ShareControlHeader {
};

if pdu_type == ShareControlPduType::DataPdu {
// Note this is the *re-encoded* size, not the bytes that were on the wire. The two
// diverge for a header-only Server Font Map: `ShareDataPdu::from_type` substitutes
// `FontPdu::default()` on an empty cursor, so the re-encoded size counts an 8-byte
// body that was never sent, and a conformant server declaring 18 was measured
// against 26. That is why an honest header-only Font Map was rejected, and it is
// the reason the exception below is keyed on the PDU type rather than on arithmetic.
let header_length = header.size();

// An empty Update/Pointer PDU is a legitimate no-op carrying a zero length.
let is_empty_output_pdu = matches!(
&header.share_control_pdu,
ShareControlPdu::Data(ShareDataHeader {
Expand All @@ -331,15 +338,26 @@ impl<'de> Decode<'de> for ShareControlHeader {
}) if data.is_empty()
);

if header_length != total_length && !(total_length == 0 && is_empty_output_pdu) {
if total_length < header_length {
return Err(not_enough_bytes_err!(total_length, header_length));
}
// VirtualBox's VRDP declares only the two headers of a Server Font Map (18) and
// never counts the 8-byte body that follows, so it under-declares by exactly the
// body it did send. Narrowed to this PDU type rather than allowed for Data PDUs at
// large, so a malformed non-output PDU declaring 1..17 is still rejected.
let is_font_map = matches!(
&header.share_control_pdu,
ShareControlPdu::Data(ShareDataHeader {
share_data_pdu: ShareDataPdu::FontMap(_),
..
})
);

// Some Windows versions append padding that is not part of the inner unit.
if total_length > header_length {

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.

MS-RDPBCGR 2.2.8.1.1.1.1 defines totalLength as the packet length including the Share Control Header, and 3.2.5.2 requires checking it for consistency (a discrepancy should cause the connection to be dropped). This condition now accepts totalLength < header_length for every Data PDU, including totalLength == 0; the existing rdp::headers::tests::reject_zero_length_non_output_data_pdu fails because a ShutdownDenied PDU with a zero length is accepted. Please retain the prior exception only for empty Update/Pointer PDUs, or narrow the compatibility handling to a complete known VRDP Font Map body rather than accepting arbitrary under-declared lengths.

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.

Still request changes: this condition leaves every positive totalLength < header_length accepted. That includes malformed non-output Data PDUs (for example, a header-only PDU declared with 1–17 bytes), not just the complete VRDP Font Map compatibility case. MS-RDPBCGR §3.2.5.2 requires the server to validate totalLength consistency; please narrow the under-declared exception to the validated Server Font Map case, or otherwise prove that the complete inner PDU is present before accepting it.

// Over-declared: some Windows versions append padding past the inner unit.
// Unchanged, and still bounded by `ensure_size!`.
let padding = total_length - header_length;
ensure_size!(in: src, size: padding);
read_padding!(src, padding);
} else if total_length < header_length && !is_font_map && !is_empty_output_pdu {
return Err(not_enough_bytes_err!(total_length, header_length));
}
}

Expand Down Expand Up @@ -947,6 +965,29 @@ mod tests {
));
}

/// The under-declaration carve-out must not extend past the Server Font Map case.
///
/// A `ShutdownDenied` PDU is header-only, so 18 bytes were read; declaring 17 is neither
/// self-consistent with the wire nor the VRDP Font Map, and MS-RDPBCGR 3.2.5.2 asks for it
/// to be rejected. Guards the exact example raised in review — "a header-only PDU declared
/// with 1–17 bytes" — which an earlier revision of this check accepted.
#[test]
fn reject_under_declared_non_output_data_pdu() {
let mut encoded = zero_length_empty_data_pdu(0x25);
encoded[0] = 17;

let error = decode::<ShareControlHeader>(&encoded)
.expect_err("an under-declared length is only tolerated for a Server Font Map");

assert!(matches!(
error.kind(),
ironrdp_core::DecodeErrorKind::NotEnoughBytes {
received: 17,
expected: 18
}
));
}

#[test]
fn share_data_context_retains_compression_metadata() {
let mut user_data = encode_vec(&ShareControlHeader {
Expand Down
27 changes: 27 additions & 0 deletions crates/ironrdp-testsuite-core/tests/pdu/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ fn from_header_only_buffer_defaults_rdp_pdu_server_font_map() {
assert_eq!(SERVER_FONT_MAP.clone(), decode(buf).unwrap());
}

/// VirtualBox's VRDP declares `totalLength` as the size of the two headers (18) and does not
/// count the 8-byte Font Map body that follows it, so the PDU is complete but under-declared.
#[test]
fn from_buffer_with_under_declared_total_length_parses_rdp_pdu_server_font_map() {
let mut buf = SERVER_FONT_MAP_BUFFER;
buf[0] = 18;

assert_eq!(SERVER_FONT_MAP.clone(), decode(buf.as_ref()).unwrap());
}

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 / medium: The new test pins only the "body present, totalLength under-declared" case. The subtler half of the fix is untested: because header.size() counts the FontPdu body that ShareDataPdu::from_type defaults in when the cursor is empty (headers.rs:625-633), master rejects an 18-byte header-only Font Map that declares a fully self-consistent totalLength of 18 (18 < 26 hits the removed branch). That is a spec-conformant encoding the old code refused, and it is the case most likely to be hit in the field. The existing from_header_only_buffer_defaults test does not cover it — its fixture still declares 26. Adding a case with an 18-byte buffer and buf[0] = 18 would pin the acceptance boundary and document why comparing against the re-encoded size, rather than bytes consumed, is what made the check unusable.


/// A header-only Server Font Map that declares its own length honestly: 18 bytes sent, 18
/// declared. Nothing about it is malformed, and it is the encoding a server is most likely to
/// send in the field.
///
/// It is pinned separately because it is the case the old check got wrong, for a reason that is
/// easy to reintroduce: `ShareDataPdu::from_type` defaults a `FontPdu` in when the cursor is
/// empty, so the re-encoded size is 26 and comparing 18 against it rejected a conformant PDU.
/// Anyone reinstating a comparison against the re-encoded size will fail here. The neighbouring
/// `from_header_only_buffer_defaults` test does not cover it — its fixture still declares 26.
#[test]
fn from_header_only_buffer_with_matching_total_length_parses_rdp_pdu_server_font_map() {
let mut buf = SERVER_FONT_MAP_BUFFER[..18].to_vec();
buf[0] = 18;

assert_eq!(SERVER_FONT_MAP.clone(), decode(buf.as_slice()).unwrap());
}

#[test]
fn from_header_only_buffer_rejects_rdp_pdu_client_font_list() {
assert!(decode::<ironrdp_pdu::rdp::headers::ShareControlHeader>(&CLIENT_FONT_LIST_BUFFER[..18]).is_err());
Expand Down
Loading