fix(egfx): drop undecodable surface updates instead of the session - #1789
fix(egfx): drop undecodable surface updates instead of the session#1789truebest wants to merge 1 commit into
Conversation
d674d57 to
6bb2242
Compare
There was a problem hiding this comment.
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()afterMAX_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_statecovering 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.mdrule that log messages capitalize the first letter.
);
| codec_context_id = pdu.codec_context_id, | ||
| bytes = pdu.bitmap_data.len(), | ||
| skipped_total = self.progressive_failures, | ||
| "skipping undecodable RFX Progressive payload" |
6bb2242 to
2b12a1b
Compare
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.
2b12a1b to
8ace185
Compare
|
Force-pushed with a follow-up the CI caught: The tool already sets a flag when its 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. |
Problem
Every decode failure in the
WireToSurfacedispatch becomes a terminal error. It leaveshandle_pdu, thenGraphicsPipelineClient::process, thenDynamicVirtualChannel::process, thenDrdynvcClient::process, so a single update the codec rejects ends the RDP session rather than the update. This holds fordecode_avc420,decode_clearcodec,decode_planarand 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)andSrl(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
drop_updatepath. 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.Testing
a_streak_of_dropped_updates_clears_the_codec_statecovers the streak, the state clearing and its reset by a good update.an_undecodable_clearcodec_update_is_dropped_not_fatalcovers a second codec through the same path.cargo test -p ironrdp-egfx: 48 passing.cargo clippy --all-targets -- -D warningsandcargo fmtclean.Note
This touches
handle_wire_to_surface1andhandle_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.