Skip to content

fix(pdu): tolerate unknown GCC user-data blocks instead of failing#1367

Open
Hani (mojo17) wants to merge 1 commit into
Devolutions:masterfrom
mojo17:up-pdu-skip-unknown-gcc
Open

fix(pdu): tolerate unknown GCC user-data blocks instead of failing#1367
Hani (mojo17) wants to merge 1 commit into
Devolutions:masterfrom
mojo17:up-pdu-skip-unknown-gcc

Conversation

@mojo17

Copy link
Copy Markdown

Summary

As an RDP server, IronRDP currently fails to negotiate a connection with
current Microsoft clients (the Windows App / msrdc): it aborts the
whole GCC decode the moment it encounters a user-data block whose
blockType it does not model, dropping the connection during the
Conference Create exchange. This change makes the GCC block decoders skip
blocks they do not recognise and continue, which is what the protocol's
wire format is designed for and what other implementations already do.

Problem

When decoding a GCC Conference Create Request/Response, ClientGccBlocks
and ServerGccBlocks return an error on the first user-data block whose
blockType is not one of the variants IronRDP models, which aborts the
entire decode and fails the connection.

The current Windows App (the msrdc client) trips this in two ways:

  1. It sends a TS_UD_CS_UNUSED1 block (blockType 0xC00C). This block
    is documented, and MS-RDPBCGR 2.2.1.3.9 states it "SHOULD be ignored
    by the server if sent by the client", but IronRDP did not model it
    (the ClientGccType set stopped at 0xC00A), so it was rejected
    rather than ignored.
  2. It also sends a block with blockType 0xC00D, which is undocumented:
    it is in neither the User Data Header type list in MS-RDPBCGR 2.2.1.3.1
    nor FreeRDP's parser. This follows the pattern of the 0xC009 block
    historically sent by the Lync RDP client, also absent from the spec.

Either block causes the decoder to reject the connection, so an
ironrdp-server peer cannot complete the connection sequence with this
client at all.

Why tolerate unknown blocks

GCC user-data blocks are a length-prefixed TLV structure: TS_UD_HEADER
is type (2 bytes) + length (2 bytes) + value. The length exists
precisely so a reader that does not recognise a type can seek past the
payload and continue with the next block, which is the standard
forward-compatibility contract for this wire format. Rejecting on an
unknown type forfeits that.

This is also how the most widely used open-source RDP implementation
behaves. FreeRDP's GCC parser logs the unknown block and continues
rather than aborting the handshake:

default:
    WLog_Print(mcs->log, WLOG_ERROR, "Unknown GCC client data block: 0x%04" PRIX16 "", type);
    winpr_HexLogDump(mcs->log, WLOG_TRACE, Stream_Pointer(sub), Stream_GetRemainingLength(sub));
    break;

It advances by the block's length and proceeds. FreeRDP also models
CS_UNUSED1 (0xC00C) as a named case and carries 0xC009 as a literal
case, matching the approach this PR takes.

Change

  • Add UserDataHeader::decode_raw, which returns the raw u16
    blockType and the block body without mapping the type to an enum.
    The block length is honoured, so the cursor can always advance past a
    block the caller does not recognise.
  • ClientGccBlocks::decode and ServerGccBlocks::decode now use it and
    skip user-data blocks they do not model instead of erroring.
  • Model TS_UD_CS_UNUSED1 (blockType 0xC00C) explicitly. Per
    MS-RDPBCGR 2.2.1.3.9 it is a padding block that "SHOULD be ignored
    by the server if sent by the client", so it is handled as a named
    no-op rather than going through the generic unknown-block path.

The existing generic UserDataHeader::decode is kept as a thin wrapper
over decode_raw, so its signature and behaviour are unchanged. This PR
is non-breaking.

Tests

  • from_buffer_skips_unknown_client_gcc_block_and_parses_known_blocks
    verifies an undocumented 0xC00D block is skipped while the known
    blocks still parse.
  • from_buffer_ignores_cs_unused1_client_gcc_block verifies a
    CS_UNUSED1 block is recognised and ignored.
  • decode_raw_returns_raw_block_type_for_unknown_type verifies
    decode_raw surfaces the raw unrecognised blockType.

As an RDP server, IronRDP fails to negotiate a connection with the
current Windows App (the msrdc client): ClientGccBlocks/ServerGccBlocks
return an error on the first GCC user-data block whose blockType is not
one of the variants IronRDP models, which aborts the whole decode and
drops the connection during the Conference Create exchange.

The Windows App trips this in two ways:

- It sends a TS_UD_CS_UNUSED1 block (blockType 0xC00C). This block is
  documented and MS-RDPBCGR 2.2.1.3.9 states it "SHOULD be ignored by
  the server if sent by the client", but IronRDP did not model it (the
  ClientGccType set stopped at 0xC00A), so it was rejected rather than
  ignored.
- It also sends blockType 0xC00D, which is undocumented: it is in
  neither the User Data Header type list in MS-RDPBCGR 2.2.1.3.1 nor
  FreeRDP's parser, mirroring the historical 0xC009 block sent by the
  Lync client.

GCC user-data blocks are a length-prefixed TLV (TS_UD_HEADER is type +
length + value), so a reader can seek past an unknown block and continue;
FreeRDP logs and skips unknown blocks rather than aborting.

Add UserDataHeader::decode_raw, which returns the raw u16 blockType and
the block body without mapping the type to an enum. The block decoders
use it and skip user-data blocks they do not recognise, advancing past
the body via the block length. The existing generic decode is retained
(now a thin wrapper over decode_raw) so its API and behavior are
unchanged. Also model TS_UD_CS_UNUSED1 (0xC00C) as an explicit no-op so
the one documented ignorable block is handled by name.

Add tests covering a skipped unknown block, an ignored CS_UNUSED1 block,
and decode_raw returning a raw unrecognised blockType.
@glamberson

Copy link
Copy Markdown
Contributor

The GCC user-data section is a length-prefixed block list, designed so a reader can step over a block it does not model, and the table in 2.2.1.3.1 is open: Microsoft has added types over the years without revising the public spec. Rejecting an unrecognized blockType is therefore a conformance bug rather than a safe default. A server that rejects such blocks cannot finish Conference Create with current Windows App and msrdc builds. I maintain an IronRDP-based server downstream, so this is the exact path that matters to me.

The shape is right. Applying it symmetrically to both ClientGccBlocks and ServerGccBlocks is correct, since the same rule governs the server blocks on the client path. Keeping decode as a thin wrapper over decode_raw preserves the enum for callers that want it while making the loop tolerant, which matches the direction the project has taken on protocol fields. Calling out CS_UNUSED1 (0xC00C) explicitly rather than folding it into the unknown path is a good distinction, since 2.2.1.3.9 documents it as ignore-on-purpose.

One cheap addition is worth considering. A trace-level log of the skipped blockType would let the next undocumented block, this one being 0xC00D, be identified from a capture without attaching a debugger. That is not a blocker; it is observability for the next time the table grows.

@mojo17

Hani (mojo17) commented Jun 10, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed read, and for confirming this from the downstream-server side. That's exactly the path this is meant to unblock.

On the trace log: I agree the observability is worth having, but I'd lean against putting it inside the decoder here. ironrdp-pdu deliberately carries no tracing/log dependency (and is no_std-capable), so logging the skipped blockType in the GCC decode loop would mean introducing a logging dependency into a layer that intentionally avoids one. To keep the PR non-breaking and dependency-neutral, decode_raw already returns the raw u16 blockType, so a caller (e.g. ironrdp-server) can log skipped blocks at trace level without the PDU crate taking on logging. Happy to follow up with that caller-side log in ironrdp-server if the maintainers prefer it live there. Would that work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants