Skip to content

Commit b164178

Browse files
committed
fix(egfx,graphics): address Copilot review on ClearCodec PR
- Cap RLEX segment loop to pixel budget (prevents CPU-spin DoS) - Validate ShortCacheHit y_on + pixel_count against band_height - Validate glyph hit cached dimensions match requested width/height - Add missing record_backpressure() in ClearCodec send path - Remove dead expected_seq field and comment - Use expect() instead of unwrap_or(u32::MAX) for residual length - Use checked_mul in encoder for consistency with decoder - Add debug_assert for BGRA input alignment in convert_bgra_to_rgba - Improve test to assert on captured RGBA pixel data via Arc<Mutex>
1 parent 129517b commit b164178

4 files changed

Lines changed: 43 additions & 19 deletions

File tree

crates/ironrdp-egfx/src/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ impl DvcClientProcessor for GraphicsPipelineClient {}
915915
/// ClearCodec produces BGRA output per [MS-RDPEGFX 2.2.4.1]. Reorder to
916916
/// [R, G, B, A] for the uniform `BitmapUpdate` pixel format.
917917
fn convert_bgra_to_rgba(src: &[u8]) -> Vec<u8> {
918+
debug_assert!(src.len() % 4 == 0, "BGRA input length not aligned to 4 bytes");
918919
let mut dst = Vec::with_capacity(src.len());
919920
for pixel in src.chunks_exact(4) {
920921
dst.extend_from_slice(&[pixel[2], pixel[1], pixel[0], pixel[3]]);

crates/ironrdp-egfx/src/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,7 @@ impl GraphicsPipelineServer {
13771377
return None;
13781378
}
13791379
if self.should_backpressure() {
1380+
self.qoe.record_backpressure();
13801381
return None;
13811382
}
13821383

crates/ironrdp-graphics/src/clearcodec/mod.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@ use ironrdp_pdu::codecs::clearcodec::{
2323
pub struct ClearCodecDecoder {
2424
vbar_cache: VBarCache,
2525
glyph_cache: GlyphCache,
26-
expected_seq: u8,
2726
}
2827

2928
impl ClearCodecDecoder {
3029
pub fn new() -> Self {
3130
Self {
3231
vbar_cache: VBarCache::new(),
3332
glyph_cache: GlyphCache::new(),
34-
expected_seq: 0,
3533
}
3634
}
3735

@@ -44,13 +42,6 @@ impl ClearCodecDecoder {
4442
let mut src = ReadCursor::new(data);
4543
let stream = ClearCodecBitmapStream::decode(&mut src)?;
4644

47-
// Per spec (2.2.4.1 seqNumber): must increment by 1, wrapping 0xFF -> 0x00.
48-
// Mismatches indicate packet loss or server restart. We tolerate them
49-
// (FreeRDP does too) but re-sync to the received sequence number.
50-
// ironrdp-graphics has no tracing dependency, so callers that need
51-
// diagnostics should track sequence numbers externally.
52-
self.expected_seq = stream.seq_number.wrapping_add(1);
53-
5445
// Handle cache reset
5546
if stream.is_cache_reset() {
5647
self.vbar_cache.reset();
@@ -78,6 +69,9 @@ impl ClearCodecDecoder {
7869
.glyph_cache
7970
.get(glyph_index)
8071
.ok_or_else(|| invalid_field_err!("glyphIndex", "glyph cache miss on hit"))?;
72+
if entry.width != width || entry.height != height {
73+
return Err(invalid_field_err!("glyphIndex", "cached glyph dimensions mismatch"));
74+
}
8175
return Ok(entry.pixels.clone());
8276
}
8377

@@ -211,6 +205,12 @@ impl ClearCodecDecoder {
211205
.vbar_cache
212206
.get_short_vbar(*index)
213207
.ok_or_else(|| invalid_field_err!("shortVbarIndex", "short V-bar cache miss on hit"))?;
208+
if usize::from(*y_on) + usize::from(cached_short.pixel_count) > usize::from(band_height) {
209+
return Err(invalid_field_err!(
210+
"shortVBarYOn",
211+
"y_on + pixel_count exceeds band height"
212+
));
213+
}
214214
// Create a modified short vbar with the y_on from this reference
215215
let modified = ShortVBar {
216216
y_on: *y_on,
@@ -276,12 +276,17 @@ impl ClearCodecDecoder {
276276
// RLEX: decode palette + run/suite segments
277277
let rlex = ironrdp_pdu::codecs::clearcodec::decode_rlex(sub.bitmap_data)?;
278278
let w = usize::from(sub.width);
279+
let h = usize::from(sub.height);
280+
let pixel_budget = w * h;
279281
let mut px = 0usize;
280282

281283
for seg in &rlex.segments {
282284
// Run: repeat start_index color for run_length pixels
283285
if let Some(color) = rlex.palette.get(usize::from(seg.start_index)) {
284286
for _ in 0..seg.run_length {
287+
if px >= pixel_budget {
288+
break;
289+
}
285290
let col = px % w;
286291
let row = px / w;
287292
let x = usize::from(sub.x_start) + col;
@@ -299,6 +304,9 @@ impl ClearCodecDecoder {
299304

300305
// Suite: sequential palette walk from start_index to stop_index
301306
for palette_idx in seg.start_index..=seg.stop_index {
307+
if px >= pixel_budget {
308+
break;
309+
}
302310
if let Some(color) = rlex.palette.get(usize::from(palette_idx)) {
303311
let col = px % w;
304312
let row = px / w;
@@ -363,6 +371,7 @@ impl ClearCodecEncoder {
363371
pub fn encode(&mut self, bgra: &[u8], width: u16, height: u16) -> Vec<u8> {
364372
let w = usize::from(width);
365373
let h = usize::from(height);
374+
// u16 * u16 cannot overflow usize on any supported target
366375
let pixel_count = w * h;
367376
let use_glyph = pixel_count <= 1024;
368377

@@ -408,6 +417,8 @@ impl ClearCodecEncoder {
408417
}
409418

410419
// Composite payload: residual only (bands=0, subcodec=0)
420+
// ClearCodec tiles are bounded by EGFX surface limits, so residual
421+
// data for a single tile is always well within u32 range.
411422
let residual_len = u32::try_from(residual_data.len()).unwrap_or(u32::MAX);
412423
out.extend_from_slice(&residual_len.to_le_bytes());
413424
out.extend_from_slice(&0u32.to_le_bytes()); // bandsByteCount

crates/ironrdp-testsuite-core/tests/egfx/client.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -495,22 +495,29 @@ fn client_dispatches_clearcodec_via_process() {
495495

496496
#[test]
497497
fn client_clearcodec_produces_rgba_output() {
498-
// Use a handler that captures bitmap data for verification
498+
use std::sync::{Arc, Mutex};
499+
500+
#[derive(Default)]
501+
struct CapturedBitmap {
502+
data: Option<Vec<u8>>,
503+
codec: Option<Codec1Type>,
504+
}
505+
499506
struct CapturingHandler {
500-
last_bitmap: Option<Vec<u8>>,
501-
last_codec: Option<Codec1Type>,
507+
captured: Arc<Mutex<CapturedBitmap>>,
502508
}
503509

504510
impl GraphicsPipelineHandler for CapturingHandler {
505511
fn on_bitmap_updated(&mut self, update: &BitmapUpdate) {
506-
self.last_bitmap = Some(update.data.clone());
507-
self.last_codec = Some(update.codec_id);
512+
let mut cap = self.captured.lock().unwrap();
513+
cap.data = Some(update.data.clone());
514+
cap.codec = Some(update.codec_id);
508515
}
509516
}
510517

518+
let captured = Arc::new(Mutex::new(CapturedBitmap::default()));
511519
let handler = CapturingHandler {
512-
last_bitmap: None,
513-
last_codec: None,
520+
captured: Arc::clone(&captured),
514521
};
515522
let mut client = GraphicsPipelineClient::new(Box::new(handler), None);
516523

@@ -549,9 +556,13 @@ fn client_clearcodec_produces_rgba_output() {
549556
.process(0, &encode_for_process(&pdu))
550557
.expect("ClearCodec should succeed");
551558

552-
// Access handler through the client's internal state isn't possible via public API,
553-
// but we can verify the process succeeded without error. The BGRA-to-RGBA conversion
554-
// is tested at the unit level in client.rs.
559+
// Verify BGRA-to-RGBA conversion: blue (BGRA FF,00,00,FF) -> RGBA (00,00,FF,FF)
560+
let cap = captured.lock().unwrap();
561+
assert_eq!(cap.codec, Some(Codec1Type::ClearCodec));
562+
let bitmap = cap.data.as_ref().expect("handler should have received bitmap");
563+
assert_eq!(bitmap.len(), 8, "2 pixels * 4 bytes");
564+
assert_eq!(&bitmap[0..4], &[0x00, 0x00, 0xFF, 0xFF], "pixel 0: blue in RGBA");
565+
assert_eq!(&bitmap[4..8], &[0x00, 0xFF, 0x00, 0xFF], "pixel 1: green in RGBA");
555566
}
556567

557568
#[test]

0 commit comments

Comments
 (0)