Skip to content

Commit 72d28d2

Browse files
author
Clint Christopher Canada
committed
feat(rdpsnd)!: misuse-resistant format negotiation for RdpsndServerHandler
The old `start(&ClientAudioFormatPdu) -> Option<u16>` made every handler compute `wFormatNo` itself, as an index into the *client's* format list. Getting it wrong (e.g. returning a server-list index) yields `wFormatNo >= NumberOfClientFormats`, which a compliant client rejects, silently dropping all audio — and each handler re-implemented the same server-vs-client intersection. Move that work into the crate and split selection from lifecycle: fn choose_format<'a>(&mut self, common: &'a [NegotiatedFormat]) -> Option<&'a NegotiatedFormat>; fn start(&mut self, format: &NegotiatedFormat); The crate computes `common` (formats from `get_formats()` the client also advertised, in the server's preference order, each tagged with its client-list `wFormatNo`), calls `choose_format`, then `start` with the chosen format. `NegotiatedFormat` has a private `wformat_no` and no public constructor, and `choose_format` returns a borrow of an element of `common`, so a handler cannot pick a format the client did not accept nor emit an out-of-range `wFormatNo`. `choose_format` is not called when nothing is in common. Separating `choose_format` (pure selection) from `start` (encoder init / producer startup) keeps the two concerns distinct. Resolves the footgun documented in #1343. Closes #1356. BREAKING CHANGE: `RdpsndServerHandler::start` is replaced by `choose_format` (selection) plus `start(&NegotiatedFormat)` (lifecycle). Implementors now return a `&NegotiatedFormat` from `choose_format` instead of computing a `wFormatNo`. - Add `NegotiatedFormat` (private `wformat_no`, `format()` accessor) and `negotiate_formats` (intersection + client-index mapping), with unit tests for client-index mapping, the PCM-only/AAC-first regression, empty overlap, and WAVEFORMATEX-identity equality. - Match formats on WAVEFORMATEX identity (tag/channels/rate/bits), ignoring derived and codec-`data` fields a client may not echo verbatim. - Update the example server to the new two-method shape.
1 parent ef20ea4 commit 72d28d2

3 files changed

Lines changed: 205 additions & 43 deletions

File tree

crates/ironrdp-rdpsnd/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ categories.workspace = true
1414

1515
[lib]
1616
doctest = false
17-
test = false
17+
# test = false
1818

1919
[features]
2020
default = []

crates/ironrdp-rdpsnd/src/server.rs

Lines changed: 192 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,77 @@ pub enum RdpsndServerMessage {
2828
Error(Box<dyn RdpsndError>),
2929
}
3030

31+
/// A server-offered audio format that the client also advertised support for,
32+
/// paired with the `wFormatNo` the client expects for it on the wire.
33+
///
34+
/// The crate computes the set of these — the intersection of the server's
35+
/// [`get_formats`] and the client's accepted formats — and hands it to
36+
/// [`RdpsndServerHandler::choose_format`], which returns the one to stream.
37+
///
38+
/// `wformat_no` is intentionally private and there is no public constructor:
39+
/// a handler can neither build nor mutate a `NegotiatedFormat`, so the index
40+
/// stamped onto every Wave/Wave2 PDU is always a valid position in the
41+
/// client's own format list. This makes it impossible to emit an out-of-range
42+
/// `wFormatNo` (which a compliant client rejects, silently dropping all audio
43+
/// — the classic footgun of the old index-returning API).
44+
///
45+
/// [`get_formats`]: RdpsndServerHandler::get_formats
46+
#[derive(Debug, Clone, PartialEq, Eq)]
47+
pub struct NegotiatedFormat {
48+
/// The negotiated audio format (common to server and client).
49+
format: pdu::AudioFormat,
50+
/// Position of `format` in the client's Client Audio Formats list — the
51+
/// `wFormatNo` the client resolves each wave against. Crate-owned.
52+
wformat_no: u16,
53+
}
54+
55+
impl NegotiatedFormat {
56+
/// The negotiated audio format — common to both server and client, and the
57+
/// one the returned wave data should match.
58+
pub fn format(&self) -> &pdu::AudioFormat {
59+
&self.format
60+
}
61+
}
62+
3163
/// Handler for the server side of the Audio Output Virtual Channel (`RDPSND`).
3264
///
33-
/// Implementations supply the list of audio formats the server offers, decide
34-
/// which format to use once the client replies, and produce the audio waves to
35-
/// stream (via [`RdpsndServer::wave`]).
65+
/// Implementations supply the list of audio formats the server offers, choose
66+
/// which negotiated format to use once the client replies, and produce the
67+
/// audio waves to stream (via [`RdpsndServer::wave`]).
3668
pub trait RdpsndServerHandler: Send + core::fmt::Debug {
3769
/// The audio formats the server advertises in the Server Audio Formats and
3870
/// Version PDU (MS-RDPEA 2.2.2.1).
3971
fn get_formats(&self) -> &[pdu::AudioFormat];
4072

41-
/// Called once the client has replied with the formats it accepts
42-
/// (`client_format`, the Client Audio Formats and Version PDU). Returns the
43-
/// `wFormatNo` to stamp on every subsequent Wave/Wave2 PDU, or [`None`] if
44-
/// no offered format is acceptable (no audio is then streamed).
73+
/// Select which format to stream, once the client has replied with the
74+
/// formats it accepts.
4575
///
46-
/// **The returned index addresses `client_format.formats` — the formats the
47-
/// client just echoed back — NOT the server's own [`get_formats`] list.**
48-
/// The client resolves each wave's format as `ClientFormats[wFormatNo]`
49-
/// against the list *it* sent, and a compliant client rejects any
50-
/// `wFormatNo >= client_format.formats.len()`, silently dropping all audio.
51-
/// The client's list is its accepted subset of the server's formats, so the
52-
/// two lists generally differ in both length and ordering; an index into
53-
/// [`get_formats`] only happens to work when the chosen format sits at the
54-
/// same position in both. Pick the format you intend to send, then return
55-
/// its position within `client_format.formats`.
76+
/// `common` is the set of formats from [`get_formats`] that the client also
77+
/// advertised, in the server's preference order; each carries the
78+
/// `wFormatNo` the client expects, so the crate — not the handler — owns
79+
/// the index arithmetic and the MS-RDPEA rule that `wFormatNo` addresses
80+
/// the *client's* list. `common` is never empty: when server and client
81+
/// share no format, this method is not called and no audio is streamed.
82+
///
83+
/// Return the [`NegotiatedFormat`] to stream (a reference borrowed from
84+
/// `common`), or [`None`] to decline. Returning a borrow from `common`
85+
/// — rather than an index or a constructed value — makes it impossible to
86+
/// pick a format the client did not accept or to produce an invalid
87+
/// `wFormatNo`. This is a pure selection step: any encoder/producer setup
88+
/// belongs in [`start`], which the crate calls next with the chosen format.
5689
///
5790
/// [`get_formats`]: RdpsndServerHandler::get_formats
58-
fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option<u16>;
91+
/// [`start`]: RdpsndServerHandler::start
92+
fn choose_format<'a>(&mut self, common: &'a [NegotiatedFormat]) -> Option<&'a NegotiatedFormat>;
93+
94+
/// Begin streaming with the `format` just selected by [`choose_format`].
95+
///
96+
/// Called once per session, immediately after a successful
97+
/// [`choose_format`]. This is the lifecycle hook: initialize encoder state,
98+
/// spawn the producer, etc. Waves are then emitted via [`RdpsndServer::wave`].
99+
///
100+
/// [`choose_format`]: RdpsndServerHandler::choose_format
101+
fn start(&mut self, format: &NegotiatedFormat);
59102

60103
/// Called when the audio stream is torn down (e.g. the client closed the
61104
/// channel or the session ended).
@@ -173,6 +216,43 @@ impl RdpsndServer {
173216
}
174217
}
175218

219+
/// Build the set of formats common to the server (`server_formats`, kept in the
220+
/// server's preference order) and the client (`client_formats`), each tagged
221+
/// with its `wFormatNo` — its index in the *client's* list, which is what the
222+
/// client resolves waves against (MS-RDPEA). The result mirrors the server's
223+
/// ordering so the handler can express preference simply by `get_formats`
224+
/// order, while the `wFormatNo` always points into the client list.
225+
fn negotiate_formats(
226+
server_formats: &[pdu::AudioFormat],
227+
client_formats: &[pdu::AudioFormat],
228+
) -> Vec<NegotiatedFormat> {
229+
server_formats
230+
.iter()
231+
.filter_map(|server_format| {
232+
client_formats
233+
.iter()
234+
.position(|client_fmt| audio_format_eq(client_fmt, server_format))
235+
.and_then(|idx| u16::try_from(idx).ok())
236+
.map(|wformat_no| NegotiatedFormat {
237+
format: server_format.clone(),
238+
wformat_no,
239+
})
240+
})
241+
.collect()
242+
}
243+
244+
/// Compare two audio formats by their WAVEFORMATEX identity — wave format tag,
245+
/// channel count, sample rate, and bit depth. Derived fields
246+
/// (`n_avg_bytes_per_sec`, `n_block_align`) and the codec-specific `data` blob
247+
/// are deliberately ignored: a client echoes back a format it accepts but is
248+
/// not guaranteed to reproduce those byte-for-byte.
249+
fn audio_format_eq(a: &pdu::AudioFormat, b: &pdu::AudioFormat) -> bool {
250+
a.format == b.format
251+
&& a.n_channels == b.n_channels
252+
&& a.n_samples_per_sec == b.n_samples_per_sec
253+
&& a.bits_per_sample == b.bits_per_sample
254+
}
255+
176256
impl_as_any!(RdpsndServer);
177257

178258
impl SvcProcessor for RdpsndServer {
@@ -220,8 +300,27 @@ impl SvcProcessor for RdpsndServer {
220300
return Ok(vec![]);
221301
};
222302
let client_format = self.client_format.as_ref().expect("available in this state");
303+
// Formats common to server and client, in the server's
304+
// preference order, each tagged with its wFormatNo (its
305+
// position in the *client's* list). Keeping this in the crate
306+
// means the handler never does index arithmetic and can't emit
307+
// an out-of-range wFormatNo.
308+
let common = negotiate_formats(self.handler.get_formats(), &client_format.formats);
223309
self.state = RdpsndState::Ready;
224-
self.format_no = self.handler.start(client_format);
310+
self.format_no = if common.is_empty() {
311+
debug!("No audio format in common with the client; audio disabled");
312+
None
313+
} else if let Some(chosen) = self.handler.choose_format(&common) {
314+
// `chosen` borrows `common` (not `self`), so the encoder
315+
// is read off it and the handler is free to borrow again
316+
// for the `start` lifecycle hook.
317+
let wformat_no = chosen.wformat_no;
318+
self.handler.start(chosen);
319+
Some(wformat_no)
320+
} else {
321+
debug!("Handler declined every common audio format; audio disabled");
322+
None
323+
};
225324
vec![]
226325
}
227326
RdpsndState::Ready => {
@@ -260,3 +359,77 @@ impl Drop for RdpsndServer {
260359
}
261360

262361
impl SvcServerProcessor for RdpsndServer {}
362+
363+
#[cfg(test)]
364+
mod tests {
365+
use super::{audio_format_eq, negotiate_formats};
366+
use crate::pdu::{AudioFormat, WaveFormat};
367+
368+
fn fmt(format: WaveFormat, rate: u32) -> AudioFormat {
369+
AudioFormat {
370+
format,
371+
n_channels: 2,
372+
n_samples_per_sec: rate,
373+
n_avg_bytes_per_sec: rate * 4,
374+
n_block_align: 4,
375+
bits_per_sample: 16,
376+
data: None,
377+
}
378+
}
379+
380+
#[test]
381+
fn wformat_no_addresses_the_client_list_not_the_server_list() {
382+
// Server prefers AAC over PCM; the client lists them in the opposite
383+
// order. wFormatNo must follow the CLIENT's indices.
384+
let server = [fmt(WaveFormat::AAC_MS, 44100), fmt(WaveFormat::PCM, 44100)];
385+
let client = [fmt(WaveFormat::PCM, 44100), fmt(WaveFormat::AAC_MS, 44100)];
386+
387+
let common = negotiate_formats(&server, &client);
388+
389+
// Ordering follows the server's preference (AAC first)...
390+
assert_eq!(common.len(), 2);
391+
assert_eq!(common[0].format().format, WaveFormat::AAC_MS);
392+
assert_eq!(common[1].format().format, WaveFormat::PCM);
393+
// ...but each wFormatNo is the position in the CLIENT list.
394+
assert_eq!(common[0].wformat_no, 1); // AAC is client index 1
395+
assert_eq!(common[1].wformat_no, 0); // PCM is client index 0
396+
}
397+
398+
#[test]
399+
fn pcm_only_client_gets_a_valid_client_index() {
400+
// Regression for the --enable-aac trap: server advertises [AAC, PCM]
401+
// but a PCM-only client must get wFormatNo 0 (its sole index), not
402+
// PCM's server-list index of 1 (which the client would reject).
403+
let server = [fmt(WaveFormat::AAC_MS, 44100), fmt(WaveFormat::PCM, 44100)];
404+
let client = [fmt(WaveFormat::PCM, 44100)];
405+
406+
let common = negotiate_formats(&server, &client);
407+
408+
assert_eq!(common.len(), 1);
409+
assert_eq!(common[0].format().format, WaveFormat::PCM);
410+
assert_eq!(common[0].wformat_no, 0);
411+
}
412+
413+
#[test]
414+
fn no_shared_format_yields_empty() {
415+
let server = [fmt(WaveFormat::OPUS, 48000)];
416+
let client = [fmt(WaveFormat::PCM, 44100)];
417+
assert!(negotiate_formats(&server, &client).is_empty());
418+
}
419+
420+
#[test]
421+
fn equality_uses_waveformatex_identity_only() {
422+
let mut a = fmt(WaveFormat::PCM, 44100);
423+
let mut b = fmt(WaveFormat::PCM, 44100);
424+
// Differ only in derived/codec fields — still the same format.
425+
b.n_avg_bytes_per_sec = 0;
426+
b.n_block_align = 99;
427+
a.data = Some(vec![1, 2, 3]);
428+
b.data = None;
429+
assert!(audio_format_eq(&a, &b));
430+
431+
// A differing identity field (sample rate) is a different format.
432+
let c = fmt(WaveFormat::PCM, 48000);
433+
assert!(!audio_format_eq(&a, &c));
434+
}
435+
}

crates/ironrdp/examples/server.rs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use std::sync::{Arc, Mutex};
1111
use anyhow::Context as _;
1212
use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory};
1313
use ironrdp::connector::DesktopSize;
14-
use ironrdp::rdpsnd::pdu::{AudioFormat, ClientAudioFormatPdu, WaveFormat};
15-
use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage};
14+
use ironrdp::rdpsnd::pdu::{AudioFormat, WaveFormat};
15+
use ironrdp::rdpsnd::server::{NegotiatedFormat, RdpsndServerHandler, RdpsndServerMessage};
1616
use ironrdp::server::tokio::sync::mpsc::UnboundedSender;
1717
use ironrdp::server::tokio::time::{self, Duration, sleep};
1818
use ironrdp::server::{
@@ -255,17 +255,6 @@ struct SndHandler {
255255
task: Option<tokio::task::JoinHandle<()>>,
256256
}
257257

258-
impl SndHandler {
259-
fn choose_format(&self, client_formats: &[AudioFormat]) -> Option<u16> {
260-
for (n, fmt) in client_formats.iter().enumerate() {
261-
if self.get_formats().contains(fmt) {
262-
return u16::try_from(n).ok();
263-
}
264-
}
265-
None
266-
}
267-
}
268-
269258
impl RdpsndServerHandler for SndHandler {
270259
fn get_formats(&self) -> &[AudioFormat] {
271260
&[
@@ -290,30 +279,32 @@ impl RdpsndServerHandler for SndHandler {
290279
]
291280
}
292281

293-
fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option<u16> {
294-
debug!(?client_format);
282+
fn choose_format<'a>(&mut self, common: &'a [NegotiatedFormat]) -> Option<&'a NegotiatedFormat> {
283+
debug!(?common);
295284

296-
let Some(nfmt) = self.choose_format(&client_format.formats) else {
297-
return Some(0);
298-
};
285+
// The crate hands us the formats common to both peers in our preference
286+
// order; take the most-preferred one.
287+
common.first()
288+
}
299289

300-
let fmt = client_format.formats[usize::from(nfmt)].clone();
290+
fn start(&mut self, format: &NegotiatedFormat) {
291+
let fmt = format.format().clone();
301292

302293
let mut opus_enc = if fmt.format == WaveFormat::OPUS {
303294
let n_channels: opus2::Channels = match fmt.n_channels {
304295
1 => opus2::Channels::Mono,
305296
2 => opus2::Channels::Stereo,
306297
n => {
307298
warn!("Invalid OPUS channels: {}", n);
308-
return Some(0);
299+
return;
309300
}
310301
};
311302

312303
match opus2::Encoder::new(fmt.n_samples_per_sec, n_channels, opus2::Application::Audio) {
313304
Ok(enc) => Some(enc),
314305
Err(err) => {
315306
warn!("Failed to create OPUS encoder: {}", err);
316-
return Some(0);
307+
return;
317308
}
318309
}
319310
} else {
@@ -348,8 +339,6 @@ impl RdpsndServerHandler for SndHandler {
348339
ts = ts.wrapping_add(100);
349340
}
350341
}));
351-
352-
Some(nfmt)
353342
}
354343

355344
fn stop(&mut self) {

0 commit comments

Comments
 (0)