Skip to content

fix(pdu): ignore undefined channel option bits instead of refusing - #1837

Open
Marynych Oleksandr (maryny4) wants to merge 1 commit into
Devolutions:masterfrom
maryny4:fix/gcc-channel-options-mask
Open

fix(pdu): ignore undefined channel option bits instead of refusing#1837
Marynych Oleksandr (maryny4) wants to merge 1 commit into
Devolutions:masterfrom
maryny4:fix/gcc-channel-options-mask

Conversation

@maryny4

Copy link
Copy Markdown
Contributor

ChannelDef::decode rejects the whole PDU when options carries a bit outside the eleven defined flags. [MS-RDPBCGR] 2.2.1.3.4.1 does not ask a server to validate that field, and for several of the flags it asks the opposite — CHANNEL_OPTION_INITIALIZED, ENCRYPT_RDP, ENCRYPT_SC and ENCRYPT_CS are each "unused and its value MUST be ignored by the server", and CHANNEL_OPTION_SHOW_PROTOCOL likewise.

So the strictness is not the protocol's, and it costs sessions.

What it costs

rdesktop 1.9.0 cannot connect to an IronRDP server at all. It writes this one field big-endian while the rest of TS_UD_CS_NET is little-endian — secure.c:516 uses out_uint32_be where every neighbouring field uses out_uint32_le — so each channel's flags arrive byte-swapped and every set bit lands outside the mask.

Captured from the wire against an IronRDP-based server:

GCC user-data header: 03 c0 44 00      type 0xC003 (CS_NET), blockLen 68 = 4 + 64
channelCount:         05 00 00 00      little-endian, correct
"cliprdr"  c0 a0 00 00   ->  read as 0x0000a0c0
"rdpsnd"   c0 00 00 00   ->  read as 0x000000c0
"snddbg"   c0 00 00 00
"rdpdr"    80 80 00 00   ->  read as 0x00008080
"drdynvc"  c0 00 00 00

Read big-endian those are exactly the constants rdesktop registers — 0xC0A00000, 0xC0000000, 0x80800000 — five channels for five. This is a client bug, not a vendor extension, but it is a client that has shipped for years, and the connection dies at GCC before a single channel is joined.

Two alternative explanations were checked against the same bytes and ruled out: the block is not misaligned (4 + 5*12 = 64 is exactly blockLen, and each name lands on an 8-byte boundary as clean NUL-terminated ASCII), and channelCount is not misread.

After

With the mask, the same client completes MCS Connect Response, Erect Domain, Attach User, all six channel joins (1002–1008), the Client Info PDU, licensing StatusValidClient and ClientConfirmActive with full capability sets, and holds a session. This single field was the whole obstacle.

Why dropping the bits is safe

Nothing on the server side reads them: ironrdp-acceptor's connection.rs matches channels by name alone, and the field it stores is still marked // TODO: what about ChannelDef?. ChannelOptions is consumed only by the client-side builder in ironrdp-svc.

Detecting the byte order and swapping would be the wrong repair — a conforming client's legitimate options can alias a byte-swapped value, so it would misparse correct input to accommodate incorrect input.

Tests

Two, both verified to fail if the strict from_bits is put back: a value mixing known and unknown bits keeps the known ones, and the wholly-undefined value rdesktop actually sends still yields a channel. cargo test -p ironrdp-pdu — 422 passed.

`ChannelDef::decode` rejected the whole PDU when `options` carried a bit
outside the eleven defined flags. [MS-RDPBCGR] 2.2.1.3.4.1 does not ask a
server to validate that field, and for several of the flags it asks the
opposite: CHANNEL_OPTION_INITIALIZED, ENCRYPT_RDP, ENCRYPT_SC and ENCRYPT_CS
are each "unused and its value MUST be ignored by the server", and
CHANNEL_OPTION_SHOW_PROTOCOL likewise. So the strictness was not the
protocol's, and it costs sessions.

rdesktop 1.9.0 cannot connect to an IronRDP server because of it. It writes
this one field big-endian while the rest of TS_UD_CS_NET is little-endian --
`secure.c:516` uses `out_uint32_be` where every neighbouring field uses
`out_uint32_le` -- so each channel's flags arrive byte-swapped and every set
bit lands outside the mask. Captured from the wire, its five channels are:

    channelCount = 05 00 00 00                 (little-endian, correct)
    "cliprdr"    c0 a0 00 00 -> read as 0x0000a0c0
    "rdpsnd"     c0 00 00 00 -> read as 0x000000c0
    "snddbg"     c0 00 00 00
    "rdpdr"      80 80 00 00 -> read as 0x00008080
    "drdynvc"    c0 00 00 00

Read big-endian those are exactly the constants rdesktop registers --
0xC0A00000, 0xC0000000, 0x80800000 -- so this is a client bug, not a vendor
extension. But it is a client that has shipped for years, and the connection
died at GCC before a single channel was joined. With this change the same
client completes MCS connect, erect domain, attach user, all six channel
joins, the Client Info PDU, licensing and ClientConfirmActive, and holds a
session.

Dropping the bits rather than keeping them is safe because nothing on the
server side reads them: `ironrdp-acceptor`'s `connection.rs` matches channels
by `name` alone, and the field it stores is still marked
`// TODO: what about ChannelDef?`. `ChannelOptions` is consumed only by the
client-side builder in `ironrdp-svc`.

Detecting the byte order and swapping would be the wrong repair: a
conforming client's legitimate options can alias a byte-swapped value, so it
would misparse correct input to accommodate incorrect input.
@github-actions github-actions Bot added maintainer-required Maintainer review or intervention is required risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny scope/core Touches the core architectural tier size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure labels Aug 29, 2026
@glamberson

Copy link
Copy Markdown
Contributor

I went through this against the spec and the sources rather than taking the body's word for it, since a change that loosens decode-side validation in core deserves that.

The spec reading is right. MS-RDPBCGR 2.2.1.3.4.1's options table defines exactly eleven flags, and five of them (CHANNEL_OPTION_INITIALIZED, ENCRYPT_RDP, ENCRYPT_SC, ENCRYPT_CS and SHOW_PROTOCOL) each carry literal "MUST be ignored by the server" language. More to the point, the server-side processing rules in 3.3.5.3.3 mandate exactly two validations for Client Network Data: The channelCount field within bounds and a complete channelDefArray. Nothing asks a server to validate the options field, so the strict from_bits here was IronRDP-only strictness with no normative backing.

The rdesktop claim checks out too: Version 1.9.0's secure.c writes this field with out_uint32_be while every neighboring field in the same block uses out_uint32_le, and the byte-swapped values in your capture match the constants rdesktop registers for its five channels. I agree that detecting and swapping would be the wrong repair; a conforming client's legitimate flag combination can alias a swapped one, so a heuristic there would misparse correct input to accommodate incorrect input.

On the fix shape, from_bits_truncate over from_bits_retain is the right choice. Nothing in the workspace decodes a ChannelDef and then re-encodes it, so nothing needs the unknown bits preserved, and truncate keeps the invariant that ChannelOptions only ever holds defined flags. It also matches the existing precedent in rdp/headers.rs, where BasicSecurityHeaderFlags already decodes with from_bits_truncate for the same interop reason. And since ironrdp-acceptor still matches channels purely by name, there is no server behavior that could regress from dropping bits it never read.

The two tests are the right two: Known plus unknown keeps the known bits, and the actual rdesktop wire value still yields the channel.

Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 30, 2026
…elds (#1846)

The two GCC core-data fields that echo the X.224 negotiation values are
decoded strictly: serverSelectedProtocol in the client core data and
clientRequestedProtocols in the server core data both reject the whole
settings exchange when the value carries a bit outside the five defined
PROTOCOL_* flags. The negotiation layer itself already decodes this
exact type with from_bits_retain in both the RDP Negotiation Request and
Response, so a value the library accepts leniently at X.224 becomes
connection-fatal when echoed back one phase later.

The strictness also works against the field's purpose. [MS-RDPBCGR]
3.3.5.3.3's mandated handling of serverSelectedProtocol is a value
comparison: if the field does not contain the same value the server
transmitted in the RDP Negotiation Response, the server SHOULD drop the
connection. That comparison needs the value to survive decode; rejecting
unknown bits at parse time replaces the spec's check with a decode
error. The client-side echo is symmetric: the client knows what it
requested and compares, rather than validating the bit set.

The exposure is demonstrated by history: the PROTOCOL_* list has grown
three times (RDSTLS, HYBRID_EX, RDSAAD), and each addition would have
made older strict decoders refuse newer peers whose negotiation had
already succeeded.

Both sites switch to from_bits_retain, the crate-wide policy since
#1144; re-encoding a decoded block reproduces the wire value.

This continues the interop line of #1489, #1458, #1837, #1843, #1844 and
#1845.

Tests: one per direction, each verifying that a fixture with an
undefined protocol bit decodes, that the bit is retained, and that
re-encoding reproduces the exact input bytes. Both fail with the strict
from_bits restored.

`cargo xtask check fmt/lints/tests/typos/locks` all pass.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 30, 2026
… PDU (#1843)

ClientInfo::decode rejects the whole Client Info PDU when the flags
field carries a bit outside the twenty-one INFO_* flags this library
defines. [MS-RDPBCGR] 3.3.5.3.11 asks the server to validate the
embedded lengths and to test the UNICODE flag before reading character
data; it never asks the server to validate the flags bit set. Microsoft
has extended the INFO_* list several times (INFO_AUDIOCAPTURE,
INFO_VIDEO_DISABLE and INFO_HIDEF_RAIL_SUPPORTED are all later
additions), so a newer client setting a bit this list does not know yet
is refused at the login step, after the connection sequence has
otherwise succeeded.

The fix switches the site to from_bits_retain, the crate-wide policy
since #1144: the unknown bits are kept rather than dropped, so
re-encoding a decoded ClientInfo reproduces the wire value. The
CompressionTypeMask bits are unaffected; they are masked out before the
flags conversion and parsed separately, as before.

This continues the same interop line as #1489 (unknown GCC user-data
blocks), #1458 (unknown security header flags), #1536/#1541
(under-declared lengths) and #1837 (undefined channel option bits).

Test: a Client Info fixture with an undefined flag bit decodes, the bit
is retained, and re-encoding reproduces the exact input bytes. Verified
to fail with the strict from_bits restored.

`cargo xtask check fmt/lints/tests/typos/locks` all pass.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 30, 2026
…s exchange (#1844)

Three GCC settings-block decoders reject the whole Basic Settings
Exchange when a flags field carries a bit outside the defined list:
RedirectionFlags in Client Cluster Data, the per-monitor flags in Client
Monitor Data, and transportFlags in the Multitransport Channel Data.
[MS-RDPBCGR] 3.3.5.3.3's processing rules mandate nothing for any of
these three fields (its checks cover the Client Network Data bounds, the
color depth fields, the serverSelectedProtocol echo, and the encryption
method flags), so the strictness is this library's own, and it fails the
connection at GCC, before any channel is joined.

The exposure is real in each case. The transportFlags list has grown
before (TRANSPORTTYPE_UDP_PREFERRED and SOFTSYNC_TCP_TO_UDP are later
additions), so an older IronRDP would have refused every client that
advertised them. The monitor flags list defines only TS_MONITOR_PRIMARY,
so any vendor or future bit refuses a multimon client outright. The
redirection flags sit next to a version mask that already anticipates
growth.

All three sites switch to from_bits_retain, the crate-wide policy since
#1144: unknown bits are kept rather than dropped, so re-encoding a
decoded block reproduces the wire value. The cluster version mask
handling is untouched, and the separately parsed RedirectionVersion
stays strict since it is a discriminant, not a bit set.

This continues the interop line of #1489 (unknown GCC user-data blocks),
#1458 (unknown security header flags), #1837 (undefined channel option
bits) and #1843 (unknown ClientInfoFlags bits).

Tests: one per site, each verifying that a fixture with an undefined bit
decodes, that the bit is retained, and that re-encoding reproduces the
exact input bytes. All three fail if the strict from_bits is restored.

`cargo xtask check fmt/lints/tests/typos/locks` all pass.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 30, 2026
…urity data (#1845)

ClientSecurityData::decode rejects the whole Basic Settings Exchange
when the encryptionMethods field carries a bit outside the four defined
flags. [MS-RDPBCGR] 2.2.1.3.3 defines the field as the set of methods
the client supports, with the server obliged to "select one of the
methods specified by the client", and [MS-RDPBCGR] 3.3.5.3.3's mandated
handling for this field is to examine encryptionMethods and
extEncryptionMethods for at least one valid flag, not to validate the
bit set. The strict decode implemented neither half of that: it rejected
a valid flag accompanied by an unknown bit, and accepted the zero-flags
case the rule actually addresses. The sibling extEncryptionMethods field
in the same structure is already read without validation. So the
strictness is this library's own, it fails the connection at GCC, and it
protects nothing: a server simply never selects a method it does not
know.

The advertisement site switches to from_bits_retain, the crate-wide
policy since #1144, with the usual property that re-encoding a decoded
block reproduces the wire value.

The server-side selection in ServerSecurityData deliberately STAYS
strict, and this change adds a comment making that asymmetry durable:
that field is the server's selected method, not an advertisement, a
conforming server can never send an unknown bit there, a client cannot
implement a method it does not know, and the value drives the
conditional serverRandom/serverCertificate parse.

This continues the interop line of #1489, #1458, #1837, #1843 and #1844.

Test: a Client Security Data fixture with an undefined method bit
decodes, the bit is retained, and re-encoding reproduces the exact input
bytes. Fails with the strict from_bits restored.

`cargo xtask check fmt/lints/tests/typos/locks` all pass.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 30, 2026
…ropping the session (#1847)

The fast-path input decoder rejects the whole event stream when a
keyboard or synchronize event carries an eventFlags bit outside the
defined set, and a decode error there is session-fatal mid-use: input
events arrive continuously, so a single unknown flag bit from a client
drops a live session with work in progress.

The strictness is also internally inconsistent: the slow-path decoders
for the very same events already tolerate unknown bits. Slow-path
keyboard flags (scan_code.rs), synchronize toggle flags (sync.rs) and
unicode keyboard flags (unicode.rs) all decode with from_bits_retain, so
the identical keystroke is tolerated on one path and connection-fatal on
the other.

The spec's only mandated strictness in this area is about event types,
not flags: [MS-RDPBCGR] 3.3.5.8.2 says the server SHOULD drop the
connection when an event structure does not match one of the known
types. That check is the existing eventCode rejection, which stays
exactly as it is. The eventFlags tables (2.2.8.1.2.2.1 and
2.2.8.1.2.2.5) carry no receiver-validation language.

All three flag sites (scancode, sync, unicode) switch to
from_bits_retain, the crate-wide policy since #1144. The retained bits
fit the 5-bit header field, so re-encoding reproduces the wire value,
and the server-side consumers read these flags with contains(), so
retained bits flow through harmlessly.

This continues the interop line of #1489, #1458, #1837, #1843, #1844,
#1845 and #1846, and it is the last flag-site conversion of that series.

Tests: a keyboard event and a synchronize event, each with an undefined
bit in the 5-bit field, decode successfully, retain the bit, and
re-encode byte-identically. Both fail with the strict from_bits
restored.

`cargo xtask check fmt/lints/tests/typos/locks` all pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer-required Maintainer review or intervention is required risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny scope/core Touches the core architectural tier size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure

Development

Successfully merging this pull request may close these issues.

2 participants