Skip to content

Commit 7113058

Browse files
glambersonlamco-office
authored andcommitted
fix(graphics,egfx): harden ClearCodec subcodec decode and fix review items
Address review feedback on Devolutions#1174: - Validate subcodec region bounds against surface dimensions - Validate RLEX palette indices and enforce region pixel budget - Remove silent pixel skipping that caused coordinate desync - Add glyph cache dimension mismatch check on hit - Use checked arithmetic for raw subcodec data length - Remove unused expected_seq field from ClearCodecDecoder - Use saturating_mul in encoder for pixel count - Add missing QoE backpressure recording for ClearCodec frames - Document vbar_cache wrap constant derivation
1 parent 961a901 commit 7113058

3 files changed

Lines changed: 105 additions & 55 deletions

File tree

crates/ironrdp-egfx/src/server.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,58 @@ impl GraphicsPipelineServer {
13561356
Some(frame_id)
13571357
}
13581358

1359+
/// Queue an uncompressed bitmap frame for transmission via EGFX
1360+
///
1361+
/// Sends raw pixel data through `WireToSurface1` with `Codec1Type::Uncompressed`.
1362+
/// Used for V8 clients that support EGFX but not H.264 (AVC420/AVC444).
1363+
///
1364+
/// The `bitmap_data` should be in the surface's pixel format (typically XRGB).
1365+
///
1366+
/// Returns `Some(frame_id)` if queued, `None` if not ready or backpressured.
1367+
pub fn send_uncompressed_frame(
1368+
&mut self,
1369+
surface_id: u16,
1370+
bitmap_data: &[u8],
1371+
dest_width: u16,
1372+
dest_height: u16,
1373+
timestamp_ms: u32,
1374+
) -> Option<u32> {
1375+
if !self.is_ready() {
1376+
return None;
1377+
}
1378+
if self.should_backpressure() {
1379+
self.qoe.record_backpressure();
1380+
return None;
1381+
}
1382+
1383+
let surface = self.surfaces.get(surface_id)?;
1384+
1385+
let timestamp = Self::make_timestamp(timestamp_ms);
1386+
let frame_id = self.frames.begin_frame(timestamp);
1387+
1388+
let dest_rect = InclusiveRectangle {
1389+
left: 0,
1390+
top: 0,
1391+
right: dest_width.saturating_sub(1),
1392+
bottom: dest_height.saturating_sub(1),
1393+
};
1394+
1395+
self.output_queue
1396+
.push_back(GfxPdu::StartFrame(StartFramePdu { timestamp, frame_id }));
1397+
1398+
self.output_queue.push_back(GfxPdu::WireToSurface1(WireToSurface1Pdu {
1399+
surface_id,
1400+
codec_id: Codec1Type::Uncompressed,
1401+
pixel_format: surface.pixel_format,
1402+
destination_rectangle: dest_rect,
1403+
bitmap_data: bitmap_data.to_vec(),
1404+
}));
1405+
1406+
self.output_queue.push_back(GfxPdu::EndFrame(EndFramePdu { frame_id }));
1407+
1408+
Some(frame_id)
1409+
}
1410+
13591411
/// Queue a ClearCodec frame for transmission.
13601412
///
13611413
/// ClearCodec is a mandatory lossless codec for all EGFX versions. It

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

Lines changed: 52 additions & 53 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

@@ -247,13 +241,22 @@ impl ClearCodecDecoder {
247241
surface_width: u16,
248242
) -> DecodeResult<()> {
249243
let sw = usize::from(surface_width);
244+
let sh = output.len() / (sw * 4).max(1);
245+
246+
let x_end = usize::from(sub.x_start) + usize::from(sub.width);
247+
let y_end = usize::from(sub.y_start) + usize::from(sub.height);
248+
if x_end > sw || y_end > sh {
249+
return Err(invalid_field_err!("subcodec", "region exceeds surface bounds"));
250+
}
250251

251252
match sub.codec_id {
252253
SubcodecId::Raw => {
253-
// Raw BGR: 3 bytes per pixel
254254
let w = usize::from(sub.width);
255255
let h = usize::from(sub.height);
256-
let expected = w * h * 3;
256+
let expected = w
257+
.checked_mul(h)
258+
.and_then(|v| v.checked_mul(3))
259+
.ok_or_else(|| invalid_field_err!("bitmapData", "raw subcodec dimensions overflow"))?;
257260
if sub.bitmap_data.len() < expected {
258261
return Err(invalid_field_err!("bitmapData", "raw subcodec data too short"));
259262
}
@@ -263,65 +266,61 @@ impl ClearCodecDecoder {
263266
let y = usize::from(sub.y_start) + row;
264267
let src_idx = (row * w + col) * 3;
265268
let dst_idx = (y * sw + x) * 4;
266-
if dst_idx + 3 < output.len() && src_idx + 2 < sub.bitmap_data.len() {
267-
output[dst_idx] = sub.bitmap_data[src_idx]; // B
268-
output[dst_idx + 1] = sub.bitmap_data[src_idx + 1]; // G
269-
output[dst_idx + 2] = sub.bitmap_data[src_idx + 2]; // R
270-
output[dst_idx + 3] = 0xFF; // A
271-
}
269+
output[dst_idx] = sub.bitmap_data[src_idx];
270+
output[dst_idx + 1] = sub.bitmap_data[src_idx + 1];
271+
output[dst_idx + 2] = sub.bitmap_data[src_idx + 2];
272+
output[dst_idx + 3] = 0xFF;
272273
}
273274
}
274275
}
275276
SubcodecId::Rlex => {
276-
// 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 region_pixels = usize::from(sub.width) * usize::from(sub.height);
280+
let palette_len = rlex.palette.len();
279281
let mut px = 0usize;
280282

281283
for seg in &rlex.segments {
282-
// Run: repeat start_index color for run_length pixels
283-
if let Some(color) = rlex.palette.get(usize::from(seg.start_index)) {
284-
for _ in 0..seg.run_length {
285-
let col = px % w;
286-
let row = px / w;
287-
let x = usize::from(sub.x_start) + col;
288-
let y = usize::from(sub.y_start) + row;
289-
let dst_idx = (y * sw + x) * 4;
290-
if dst_idx + 3 < output.len() {
291-
output[dst_idx] = color[0]; // B
292-
output[dst_idx + 1] = color[1]; // G
293-
output[dst_idx + 2] = color[2]; // R
294-
output[dst_idx + 3] = 0xFF;
295-
}
296-
px += 1;
284+
if usize::from(seg.start_index) >= palette_len {
285+
return Err(invalid_field_err!("rlex", "start_index exceeds palette size"));
286+
}
287+
if usize::from(seg.stop_index) >= palette_len {
288+
return Err(invalid_field_err!("rlex", "stop_index exceeds palette size"));
289+
}
290+
291+
let color = &rlex.palette[usize::from(seg.start_index)];
292+
for _ in 0..seg.run_length {
293+
if px >= region_pixels {
294+
return Err(invalid_field_err!("rlex", "run exceeds region pixel count"));
297295
}
296+
let x = usize::from(sub.x_start) + px % w;
297+
let y = usize::from(sub.y_start) + px / w;
298+
let dst_idx = (y * sw + x) * 4;
299+
output[dst_idx] = color[0];
300+
output[dst_idx + 1] = color[1];
301+
output[dst_idx + 2] = color[2];
302+
output[dst_idx + 3] = 0xFF;
303+
px += 1;
298304
}
299305

300-
// Suite: sequential palette walk from start_index to stop_index
301306
for palette_idx in seg.start_index..=seg.stop_index {
302-
if let Some(color) = rlex.palette.get(usize::from(palette_idx)) {
303-
let col = px % w;
304-
let row = px / w;
305-
let x = usize::from(sub.x_start) + col;
306-
let y = usize::from(sub.y_start) + row;
307-
let dst_idx = (y * sw + x) * 4;
308-
if dst_idx + 3 < output.len() {
309-
output[dst_idx] = color[0];
310-
output[dst_idx + 1] = color[1];
311-
output[dst_idx + 2] = color[2];
312-
output[dst_idx + 3] = 0xFF;
313-
}
314-
px += 1;
307+
if px >= region_pixels {
308+
return Err(invalid_field_err!("rlex", "suite exceeds region pixel count"));
315309
}
310+
let color = &rlex.palette[usize::from(palette_idx)];
311+
let x = usize::from(sub.x_start) + px % w;
312+
let y = usize::from(sub.y_start) + px / w;
313+
let dst_idx = (y * sw + x) * 4;
314+
output[dst_idx] = color[0];
315+
output[dst_idx + 1] = color[1];
316+
output[dst_idx + 2] = color[2];
317+
output[dst_idx + 3] = 0xFF;
318+
px += 1;
316319
}
317320
}
318321
}
319322
SubcodecId::NsCodec => {
320-
// NSCodec (MS-RDPNSC) decode not yet implemented.
321-
// Encoder avoids generating NSCodec tiles, so this path only
322-
// triggers when decoding streams from other implementations.
323-
// The region is left at its current pixel values (transparent
324-
// or whatever the residual layer filled in).
323+
// Not yet implemented; encoder avoids generating NSCodec tiles.
325324
}
326325
}
327326

@@ -363,7 +362,7 @@ impl ClearCodecEncoder {
363362
pub fn encode(&mut self, bgra: &[u8], width: u16, height: u16) -> Vec<u8> {
364363
let w = usize::from(width);
365364
let h = usize::from(height);
366-
let pixel_count = w * h;
365+
let pixel_count = w.saturating_mul(h);
367366
let use_glyph = pixel_count <= 1024;
368367

369368
// Check glyph cache for exact match

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
1010
use ironrdp_pdu::codecs::clearcodec::{SHORT_VBAR_CACHE_SIZE, VBAR_CACHE_SIZE};
1111

12-
// Cache sizes as u16 for cursor wrapping arithmetic.
13-
// Must match VBAR_CACHE_SIZE (32,768) and SHORT_VBAR_CACHE_SIZE (16,384).
12+
// VBAR_CACHE_SIZE (32,768) and SHORT_VBAR_CACHE_SIZE (16,384) as u16 for cursor wrapping.
1413
const VBAR_WRAP: u16 = 32_768;
1514
const SHORT_VBAR_WRAP: u16 = 16_384;
1615

0 commit comments

Comments
 (0)