fix(pdu): ignore undefined channel option bits instead of refusing - #1837
fix(pdu): ignore undefined channel option bits instead of refusing#1837Marynych Oleksandr (maryny4) wants to merge 1 commit into
Conversation
`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.
|
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. |
…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.
… 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.
…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.
…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.
…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.
ChannelDef::decoderejects the whole PDU whenoptionscarries 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_SCandENCRYPT_CSare each "unused and its value MUST be ignored by the server", andCHANNEL_OPTION_SHOW_PROTOCOLlikewise.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_NETis little-endian —secure.c:516usesout_uint32_bewhere every neighbouring field usesout_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:
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 = 64is exactlyblockLen, and each name lands on an 8-byte boundary as clean NUL-terminated ASCII), andchannelCountis 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
StatusValidClientandClientConfirmActivewith 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'sconnection.rsmatches channels bynamealone, and the field it stores is still marked// TODO: what about ChannelDef?.ChannelOptionsis consumed only by the client-side builder inironrdp-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_bitsis 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.