Skip to content

fix(egfx): drop undecodable surface updates instead of the session - #1789

Open
truebest wants to merge 1 commit into
Devolutions:masterfrom
truebest:feat/egfx-skip-undecodable-progressive
Open

fix(egfx): drop undecodable surface updates instead of the session#1789
truebest wants to merge 1 commit into
Devolutions:masterfrom
truebest:feat/egfx-skip-undecodable-progressive

Conversation

@truebest

@truebest truebest commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Every decode failure in the WireToSurface dispatch becomes a terminal error. It leaves handle_pdu, then GraphicsPipelineClient::process, then DynamicVirtualChannel::process, then DrdynvcClient::process, so a single update the codec rejects ends the RDP session rather than the update. This holds for decode_avc420, decode_clearcodec, decode_planar and the Progressive path alike.

The data is server controlled and the decoders have several reachable error paths, so this is not only a hostile-input concern. Replaying a live session against a Windows host, its Progressive stream is rejected with Srl(MissingTerminator) and Srl(Truncated), and the session died on the first frame every time. The same run survives with this change, having dropped exactly one update in two minutes.

This is the finding Benoît Cortier (@CBenoit)'s automated review raised as blocking on #1443, which I resolved at the time as a master-wide question rather than that PR's. It is master-wide, so it is fixed for the whole dispatch here.

Changes

  • AVC420, ClearCodec, Planar and Progressive go through one drop_update path. The update is lost, which costs the region it carried until the next repaint; no decoder commits partial state on failure (TileState::decode_upgrade, for instance, works on a copy and commits only on success), so later updates decode against valid state.
  • The first drop and every 64th after it are logged with the codec, surface, size and a running total, so this cannot quietly hide a decoder defect.
  • Dropping forever would leave a region frozen with no way back, so a streak of 16 consecutive drops from one codec clears that codec's state: the H.264 decoder is reset, the ClearCodec V-bar and glyph caches are released, and the Progressive contexts are dropped so the next CONTEXT rebuilds them. Planar carries no state across updates. A successful decode clears the streak.

Testing

  • a_streak_of_dropped_updates_clears_the_codec_state covers the streak, the state clearing and its reset by a good update.
  • an_undecodable_clearcodec_update_is_dropped_not_fatal covers a second codec through the same path.
  • The existing context-teardown tests now assert that the continuation is dropped rather than fatal, and that the decoder still rejected it, which is what they were really checking.

cargo test -p ironrdp-egfx: 48 passing. cargo clippy --all-targets -- -D warnings and cargo fmt clean.

Note

This touches handle_wire_to_surface1 and handle_wire_to_surface2, which #1443 also modifies, so whichever lands second needs a small rebase. Kept separate because #1443 is already near the diff-size limit.

Copilot AI balanced review requested due to automatic review settings August 24, 2026 00:13
@truebest
truebest deployed to llm-providers August 24, 2026 00:14 — with GitHub Actions Active
@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 24, 2026
@truebest
truebest force-pushed the feat/egfx-skip-undecodable-progressive branch from d674d57 to 6bb2242 Compare August 24, 2026 00:17
@truebest
truebest deployed to llm-providers August 24, 2026 00:18 — with GitHub Actions Active

Copilot AI left a comment

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.

Pull request overview

This PR changes how the EGFX graphics pipeline client reacts to an undecodable RemoteFX Progressive payload in handle_wire_to_surface2. Previously, any ProgressiveDecodeError propagated all the way up and terminated the RDP session. Since the payload is server-controlled and the decoder has several reachable error paths (observed against a live Windows host producing Srl(MissingTerminator)/Srl(Truncated)), a single rejected payload killed the session. The change makes the client skip the offending payload instead, log the failure with throttling, and drop the codec state after a bounded streak of consecutive failures so a stuck region can recover — mirroring FreeRDP's channel-rebuild behavior.

Changes:

  • Skip an undecodable Progressive payload (return Ok(())) rather than turning it into a fatal error; tile state is untouched on failure so later payloads still decode.
  • Track a lifetime failure count (for throttled logging: first failure and every 64th) and a consecutive-failure count that triggers ProgressiveDecoder::reset() after MAX_PROGRESSIVE_FAILURES_IN_A_ROW (16).
  • Update context-teardown tests to assert the continuation is skipped (not fatal) and add repeated_progressive_failures_drop_the_decoder_state covering the streak, reset, and recovery.
Suppressed comments (1)

crates/ironrdp-egfx/src/client.rs:891

  • Log message should start with a capital letter to match the convention used throughout this file and the STYLE.md rule that log messages capitalize the first letter.
                    );

Comment thread crates/ironrdp-egfx/src/client.rs Outdated
codec_context_id = pdu.codec_context_id,
bytes = pdu.bitmap_data.len(),
skipped_total = self.progressive_failures,
"skipping undecodable RFX Progressive payload"
@truebest
truebest force-pushed the feat/egfx-skip-undecodable-progressive branch from 6bb2242 to 2b12a1b Compare August 24, 2026 00:24
@truebest truebest changed the title fix(egfx): skip undecodable Progressive payloads fix(egfx): drop undecodable surface updates instead of the session Aug 24, 2026
@truebest
truebest deployed to llm-providers August 24, 2026 00:25 — with GitHub Actions Active
Every decode failure in the WireToSurface dispatch becomes a terminal error
that leaves handle_pdu, GraphicsPipelineClient::process and
DrdynvcClient::process, so one update the codec rejects ends the RDP session.
The data is server controlled and the decoders have reachable error paths that
real servers hit: a Windows host's Progressive stream is rejected with
Srl(MissingTerminator) and Srl(Truncated), which killed the session on the
first frame.

Route AVC420, ClearCodec, Planar and Progressive through one path that drops
the update instead. It costs the region the update carried, which the next
repaint covers, and no decoder commits partial state on failure, so later
updates decode against valid state. The first drop and every 64th after it are
logged with a running total, so this cannot quietly hide a decoder defect.

Dropping forever would leave a region frozen with no way back, so a streak of
16 consecutive drops from one codec clears that codec's state: the H.264
decoder is reset, the ClearCodec caches are released, and the Progressive
contexts are dropped so the next CONTEXT rebuilds them. Planar carries no
state across updates.
@truebest
truebest force-pushed the feat/egfx-skip-undecodable-progressive branch from 2b12a1b to 8ace185 Compare August 24, 2026 00:41
@truebest

Copy link
Copy Markdown
Contributor Author

Force-pushed with a follow-up the CI caught: ironrdp-capture-replay reported an unsupported EGFX codec by relying on the decode error reaching ActiveStage, which this change stops. reports_avc420_as_an_unsupported_replay_gap failed on that.

The tool already sets a flag when its UnsupportedH264Decoder refuses a frame, so the router now reads that flag on the success path and records the same Unsupported gap. cargo test -p ironrdp-capture-replay: 54 passing.

That regression is worth noting on its own: a consumer had come to depend on a decode failure killing the channel to learn that a codec was unsupported. Reading it from the decoder is the more direct signal, and it survives the session either way.

@truebest
truebest deployed to llm-providers August 24, 2026 00:42 — with GitHub Actions Active
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