From da14873ebac72274491cd4eab4d066bd0504e0ef Mon Sep 17 00:00:00 2001 From: nightness Date: Wed, 1 Apr 2026 07:42:35 -0500 Subject: [PATCH] fix(rtp): add marshal-side bounds checks for CSRC count, extension lengths, and NALU sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 3550 §5.1: CC is a 4-bit field (max 15 CSRCs); return TooManyCSRCs error if exceeded. RFC 8285 §4.2/4.3: one-byte extension payload must be 1–16 bytes, two-byte max 255 bytes; return errors rather than silently truncating via `as u8` cast. H.264/H.265 aggregation length fields are u16; skip NALUs > 65535 bytes instead of silently truncating their length field. Co-Authored-By: Claude Sonnet 4.6 --- rtc-rtp/src/codec/h264/mod.rs | 30 +++++++++++++++++------------- rtc-rtp/src/codec/h265/mod.rs | 4 ++++ rtc-rtp/src/header.rs | 20 +++++++++++++++++--- rtc-shared/src/error.rs | 8 ++++++++ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/rtc-rtp/src/codec/h264/mod.rs b/rtc-rtp/src/codec/h264/mod.rs index a18249ac..128be4ca 100644 --- a/rtc-rtp/src/codec/h264/mod.rs +++ b/rtc-rtp/src/codec/h264/mod.rs @@ -67,20 +67,24 @@ impl H264Payloader { self.pps_nalu = Some(nalu.clone()); return; } else if let (Some(sps_nalu), Some(pps_nalu)) = (&self.sps_nalu, &self.pps_nalu) { - // Pack current NALU with SPS and PPS as STAP-A - let sps_len = (sps_nalu.len() as u16).to_be_bytes(); - let pps_len = (pps_nalu.len() as u16).to_be_bytes(); - - let mut stap_a_nalu = Vec::with_capacity(1 + 2 + sps_nalu.len() + 2 + pps_nalu.len()); - stap_a_nalu.push(OUTPUT_STAP_AHEADER); - stap_a_nalu.extend(sps_len); - stap_a_nalu.extend_from_slice(sps_nalu); - stap_a_nalu.extend(pps_len); - stap_a_nalu.extend_from_slice(pps_nalu); - if stap_a_nalu.len() <= mtu { - payloads.push(Bytes::from(stap_a_nalu)); + // Pack current NALU with SPS and PPS as STAP-A. + // STAP-A length fields are u16; only pack if both NALUs fit within 65535 bytes. + if sps_nalu.len() <= u16::MAX as usize && pps_nalu.len() <= u16::MAX as usize { + let sps_len = (sps_nalu.len() as u16).to_be_bytes(); + let pps_len = (pps_nalu.len() as u16).to_be_bytes(); + + let mut stap_a_nalu = + Vec::with_capacity(1 + 2 + sps_nalu.len() + 2 + pps_nalu.len()); + stap_a_nalu.push(OUTPUT_STAP_AHEADER); + stap_a_nalu.extend(sps_len); + stap_a_nalu.extend_from_slice(sps_nalu); + stap_a_nalu.extend(pps_len); + stap_a_nalu.extend_from_slice(pps_nalu); + if stap_a_nalu.len() <= mtu { + payloads.push(Bytes::from(stap_a_nalu)); + } } - } + } // else if let (Some(sps_nalu), Some(pps_nalu)) if self.sps_nalu.is_some() && self.pps_nalu.is_some() { self.sps_nalu = None; diff --git a/rtc-rtp/src/codec/h265/mod.rs b/rtc-rtp/src/codec/h265/mod.rs index 1fd4317a..8bf8ca83 100644 --- a/rtc-rtp/src/codec/h265/mod.rs +++ b/rtc-rtp/src/codec/h265/mod.rs @@ -127,6 +127,10 @@ impl HevcPayloader { ); aggr_nalu.extend_from_slice(&header); for nalu in nalus.drain(..) { + // Aggregation unit length field is u16; skip oversized NALUs. + if nalu.len() > u16::MAX as usize { + continue; + } aggr_nalu.extend_from_slice(&(nalu.len() as u16).to_be_bytes()); aggr_nalu.extend_from_slice(&nalu); } diff --git a/rtc-rtp/src/header.rs b/rtc-rtp/src/header.rs index 3bf56959..76e9f2eb 100644 --- a/rtc-rtp/src/header.rs +++ b/rtc-rtp/src/header.rs @@ -251,7 +251,11 @@ impl Marshal for Header { return Err(Error::ErrBufferTooSmall); } - // The first byte contains the version, padding bit, extension bit, and csrc size + // The first byte contains the version, padding bit, extension bit, and csrc size. + // RFC 3550 §5.1: CC is a 4-bit field, so at most 15 contributing sources are allowed. + if self.csrc.len() > 15 { + return Err(Error::TooManyCSRCs(self.csrc.len())); + } let mut b0 = (self.version << VERSION_SHIFT) | self.csrc.len() as u8; if self.padding { b0 |= 1 << PADDING_SHIFT; @@ -296,15 +300,25 @@ impl Marshal for Header { // RFC 8285 RTP One Byte Header Extension EXTENSION_PROFILE_ONE_BYTE => { for extension in &self.extensions { - buf.put_u8((extension.id << 4) | (extension.payload.len() as u8 - 1)); + // RFC 8285 §4.2: payload must be 1–16 bytes; the length field encodes (len-1). + let len = extension.payload.len(); + if len == 0 || len > 16 { + return Err(Error::OneByteHeaderExtensionPayloadTooLarge(len)); + } + buf.put_u8((extension.id << 4) | (len as u8 - 1)); buf.put(&*extension.payload); } } // RFC 8285 RTP Two Byte Header Extension EXTENSION_PROFILE_TWO_BYTE => { for extension in &self.extensions { + // RFC 8285 §4.3: length field is one byte, so max payload is 255 bytes. + let len = extension.payload.len(); + if len > 255 { + return Err(Error::TwoByteHeaderExtensionPayloadTooLarge(len)); + } buf.put_u8(extension.id); - buf.put_u8(extension.payload.len() as u8); + buf.put_u8(len as u8); buf.put(&*extension.payload); } } diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index d319f59b..2ae72758 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -276,6 +276,14 @@ pub enum Error { #[error("extension_payload must be in 32-bit words")] HeaderExtensionPayloadNot32BitWords, + #[error("too many CSRCs: {0} exceeds the 4-bit CC field maximum of 15")] + TooManyCSRCs(usize), + #[error("one-byte header extension payload length {0} exceeds RFC 8285 maximum of 16 bytes")] + OneByteHeaderExtensionPayloadTooLarge(usize), + #[error("two-byte header extension payload length {0} exceeds maximum of 255 bytes")] + TwoByteHeaderExtensionPayloadTooLarge(usize), + #[error("NALU length {0} exceeds u16::MAX (65535 bytes)")] + NaluTooLarge(usize), #[error("audio level overflow")] AudioLevelOverflow, #[error("playout delay overflow")]