Skip to content
File

Blob: firmware/vendor/str0m/tests/malformed-packet-injection.rs

rust243 lines
1//! Regression tests for panics reachable from packets a peer (or, for the SRTP/SRTCP
2//! cases, anyone who can send a datagram to the socket) puts on the wire.
3//!
4//! str0m's policy is that a panic means a bug in str0m, never bad input. Every test
5//! here feeds input through a public API and asserts we survive it.
6 
7use std::net::{Ipv4Addr, SocketAddr};
8use std::panic::AssertUnwindSafe;
9use std::time::Instant;
10 
11use str0m::media::MediaKind;
12use str0m::net::{Protocol, Receive};
13use str0m::rtp::Ssrc;
14use str0m::rtp::rtcp::{Rtcp, Twcc};
15use str0m::{Input, RtcError};
16 
17mod common;
18use common::{TestRtc, connect_l_r, init_crypto_default, init_log};
19 
20const L_ADDR: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 1000);
21const R_ADDR: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)), 2000);
22 
23/// Feed one raw datagram straight into `Rtc::handle_input`, exactly as a socket read would.
24///
25/// Returns `None` when the demuxer rejects the datagram outright (not our concern here).
26fn inject(rtc: &mut TestRtc, bytes: &[u8]) -> Option<Result<(), RtcError>> {
27 let now = rtc.last;
28 let contents = bytes.try_into().ok()?;
29 
30 let input = Input::Receive(
31 now,
32 Receive {
33 proto: Protocol::Udp,
34 source: L_ADDR,
35 destination: R_ADDR,
36 contents,
37 },
38 );
39 
40 Some(rtc.rtc.handle_input(input))
41}
42 
43/// Run `f` and report a panic as `Err(message)` rather than unwinding out of the test,
44/// so a sweep can report *every* bad length instead of stopping at the first.
45fn catch(f: impl FnOnce()) -> Result<(), String> {
46 let prev = std::panic::take_hook();
47 std::panic::set_hook(Box::new(|_| {}));
48 let res = std::panic::catch_unwind(AssertUnwindSafe(f));
49 std::panic::set_hook(prev);
50 
51 res.map_err(|e| {
52 e.downcast_ref::<String>()
53 .cloned()
54 .or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
55 .unwrap_or_else(|| "<non-string panic>".to_string())
56 })
57}
58 
59/// Build a bare RTCP-shaped datagram of exactly `len` bytes.
60///
61/// byte0 = 0x80 and byte1 = 201 (Receiver Report) is what `MultiplexKind` uses to route
62/// a datagram to the SRTCP path, so this is all it takes to reach `unprotect_rtcp`.
63fn rtcp_shaped(len: usize) -> Vec<u8> {
64 let mut v = vec![0u8; len];
65 if len > 0 {
66 v[0] = 0x80;
67 }
68 if len > 1 {
69 v[1] = 201;
70 }
71 v
72}
73 
74/// Build an RTP-shaped datagram: 12-byte minimal header (no CSRC, no extension)
75/// plus `payload_len` bytes of payload.
76fn rtp_shaped(pt: u8, ssrc: Ssrc, seq: u16, payload_len: usize) -> Vec<u8> {
77 let mut v = Vec::with_capacity(12 + payload_len);
78 v.push(0x80); // version 2, no padding, no extension, csrc count 0
79 v.push(pt); // marker 0
80 v.extend_from_slice(&seq.to_be_bytes());
81 v.extend_from_slice(&0u32.to_be_bytes()); // timestamp
82 v.extend_from_slice(&(*ssrc).to_be_bytes());
83 v.resize(12 + payload_len, 0);
84 v
85}
86 
87/// A datagram that demuxes as SRTCP but is too short to hold the SRTCP header,
88/// the SRTCP index and the AEAD tag must be dropped, not panicked on.
89///
90/// The guard in `SrtpContext::unprotect_rtcp` only requires `SRTCP_INDEX_LEN + TAG_LEN`
91/// (20 bytes), but the code then builds `vec![0; buf.len() - TAG_LEN - SRTCP_INDEX_LEN]`
92/// and immediately writes 8 bytes into it. For 20..28 bytes that vec is shorter than 8.
93///
94/// This is reachable *before any authentication*: `Rtc::do_handle_receive` routes RTCP
95/// straight to the session with no source or ICE check, and `unprotect_rtcp` is the
96/// first code to touch the bytes.
97#[test]
98fn srtcp_shorter_than_srtcp_overhead() {
99 init_log();
100 init_crypto_default();
101 
102 let (_l, mut r) = connect_l_r();
103 
104 let mut panics = Vec::new();
105 
106 for len in 3..=64usize {
107 let packet = rtcp_shaped(len);
108 
109 if let Err(msg) = catch(|| {
110 // Ignore the Result, only the absence of a panic matters.
111 let _ = inject(&mut r, &packet);
112 }) {
113 panics.push((len, msg));
114 }
115 }
116 
117 assert!(
118 panics.is_empty(),
119 "short SRTCP datagrams panicked at these lengths: {:#?}",
120 panics
121 );
122}
123 
124/// An RTP packet whose payload is shorter than the AEAD tag must be dropped.
125///
126/// `unprotect_rtp` guards on `buf.len() < TAG_LEN`, but then computes
127/// `input.len() - TAG_LEN` where `input` is everything *after* the RTP header. A 12-byte
128/// header plus a 4..15 byte payload underflows: a subtract-overflow panic in debug, and in
129/// release a wrapped `out_len` that makes `rx_scratch.resize()` abort.
130///
131/// Also pre-authentication, since for the GCM profiles the decrypt *is* the authentication.
132#[test]
133fn srtp_payload_shorter_than_aead_tag() {
134 init_log();
135 init_crypto_default();
136 
137 let (_l, mut r) = connect_l_r();
138 
139 let mid = "aud".into();
140 let ssrc: Ssrc = 42.into();
141 r.direct_api().declare_media(mid, MediaKind::Audio);
142 r.direct_api().expect_stream_rx(ssrc, None, mid, None);
143 
144 let pt = *r.params_opus().pt();
145 
146 let mut panics = Vec::new();
147 
148 for payload_len in 0..=32usize {
149 // A fresh seq each time so the replay/dupe filter doesn't short-circuit us.
150 let packet = rtp_shaped(pt, ssrc, payload_len as u16, payload_len);
151 
152 if let Err(msg) = catch(|| {
153 let _ = inject(&mut r, &packet);
154 }) {
155 panics.push((payload_len, msg));
156 }
157 }
158 
159 assert!(
160 panics.is_empty(),
161 "short RTP payloads panicked at these payload lengths: {:#?}",
162 panics
163 );
164}
165 
166/// REMB carries an attacker-controlled SSRC count in a single byte, and the parser
167/// indexes `buf[16 + i * 4 ..]` for each of them without checking the buffer holds them.
168///
169/// Reachable by an SRTCP-authenticated peer, i.e. the remote side of any call.
170#[test]
171fn remb_ssrc_count_beyond_buffer() {
172 init_log();
173 
174 // RTCP header: version 2, fmt 15 (application layer), PT 206 (payload specific feedback).
175 let mut buf = vec![0x80 | 15, 206, 0, 4];
176 buf.extend_from_slice(&1u32.to_be_bytes()); // sender ssrc
177 buf.extend_from_slice(&0u32.to_be_bytes()); // media ssrc, must be zero
178 buf.extend_from_slice(b"REMB"); // unique identifier
179 buf.push(255); // ssrc count: claims 255 trailing SSRCs...
180 buf.extend_from_slice(&[0x1a, 0x20, 0xdf]); // exp + mantissa
181 
182 assert_eq!(buf.len(), 20, "...but the packet ends right here");
183 
184 let res = catch(|| {
185 // Must be an error, never a panic.
186 let _ = Rtcp::try_from(&buf[..]);
187 });
188 
189 assert!(
190 res.is_ok(),
191 "parsing REMB with an oversized ssrc count panicked: {}",
192 res.unwrap_err()
193 );
194 
195 // It must not come out as a REMB. (Falling through to the generic
196 // application-specific feedback parser is fine.)
197 assert!(
198 !matches!(Rtcp::try_from(&buf[..]), Ok(Rtcp::Remb(_))),
199 "a REMB claiming 255 SSRCs in a 20 byte packet must be rejected"
200 );
201}
202 
203/// A TWCC run-length vector chunk encodes each packet status in 2 bits, so `0b11` is
204/// perfectly representable on the wire. The parser accepts it (mapping it to
205/// `PacketStatus::Unknown` and consuming no delta), but `TwccIter::next` has no arm for
206/// `Unknown` and falls through to `unreachable!()`.
207///
208/// Reachable by an SRTCP-authenticated peer.
209#[test]
210fn twcc_vector_double_with_unknown_symbol() {
211 init_log();
212 
213 // RTCP header: version 2, fmt 15 (transport wide), PT 205 (transport layer feedback).
214 let mut buf = vec![0x80 | 15, 205, 0, 5];
215 buf.extend_from_slice(&1u32.to_be_bytes()); // sender ssrc
216 buf.extend_from_slice(&2u32.to_be_bytes()); // media ssrc
217 buf.extend_from_slice(&0u16.to_be_bytes()); // base seq
218 buf.extend_from_slice(&7u16.to_be_bytes()); // status count: one full VectorDouble
219 buf.extend_from_slice(&[0, 0, 0]); // reference time (24 bit)
220 buf.push(0); // feedback count
221 
222 // 0b11 in the top bits marks a "vector, 2-bit symbols" chunk. The first symbol is
223 // 0b11, the remaining six are 0b00 (not received).
224 buf.extend_from_slice(&0xF000u16.to_be_bytes());
225 
226 let Ok(Rtcp::Twcc(twcc)) = Rtcp::try_from(&buf[..]) else {
227 panic!("a well-formed TWCC chunk should parse");
228 };
229 let _: Twcc = twcc.clone();
230 
231 let res = catch(move || {
232 let count = twcc.into_iter(Instant::now(), 0.into()).count();
233 // Reaching here at all is the point.
234 let _ = count;
235 });
236 
237 assert!(
238 res.is_ok(),
239 "iterating a TWCC report with a 0b11 vector symbol panicked: {}",
240 res.unwrap_err()
241 );
242}