From e43436364fa7d6cb01a5acfe59379c69b6f69e05 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 02:03:42 +0000 Subject: [PATCH 01/17] Cap E-RTMP multitrack sub-track count to prevent CPU amplification Reject multitrack containers with more than 4096 sub-tracks, matching the existing aggregate sub-tag cap. A single 4 MiB message could otherwise encode ~1M zero-byte tracks and force millions of callback iterations per poll tick. Co-authored-by: Alexander Wagner --- src/ertmp/multitrack_media.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/ertmp/multitrack_media.rs b/src/ertmp/multitrack_media.rs index 2c8292c..bf9698f 100644 --- a/src/ertmp/multitrack_media.rs +++ b/src/ertmp/multitrack_media.rs @@ -7,6 +7,9 @@ use crate::types::FrameType; pub const ERTMP_AUDIO_PACKET_TYPE_MULTITRACK: u8 = 5; pub const ERTMP_VIDEO_PACKET_TYPE_MULTITRACK: u8 = 6; +/// Cap sub-tracks unpacked from a single multitrack container (mirrors +/// `message::message::MAX_AGGREGATE_SUBTAGS` and `session::conn::MAX_AGGREGATE_SUBTAGS`). +pub const MAX_MULTITRACK_SUBTRACKS: usize = 4096; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -106,6 +109,9 @@ pub fn foreach_track( if pos + track_size > payload.len() { return false; } + if tracks.len() >= MAX_MULTITRACK_SUBTRACKS { + return false; + } tracks.push(MediaTrackSlice { track_id, packet_type: inner_packet_type, @@ -217,4 +223,30 @@ mod tests { let payload = vec![0x96, 0x13, b'a', b'v', b'c', b'1', 0, 0, 0, 1, 0xAA]; assert!(multitrack_has_keyframe(&payload)); } + + fn build_many_tracks_zero_payload_message(track_count: usize) -> Vec { + let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1']; + for id in 0..track_count { + payload.push(id as u8); + payload.extend_from_slice(&[0x00, 0x00, 0x00]); + } + payload + } + + #[test] + fn rejects_multitrack_messages_with_too_many_subtracks() { + let at_limit = build_many_tracks_zero_payload_message(MAX_MULTITRACK_SUBTRACKS); + let mut calls = 0; + assert!(foreach_track(FrameType::Video, &at_limit, |_| calls += 1)); + assert_eq!(calls, MAX_MULTITRACK_SUBTRACKS); + + let over_limit = build_many_tracks_zero_payload_message(MAX_MULTITRACK_SUBTRACKS + 1); + let mut over_calls = 0; + assert!(!foreach_track( + FrameType::Video, + &over_limit, + |_| over_calls += 1 + )); + assert_eq!(over_calls, 0); + } } From a8cbdd22bd07ac21eed637f62c46c587a65e8e97 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:37:32 +0200 Subject: [PATCH 02/17] chore: apply PR 132 review fix --- .github/workflows/apply-pr132-review-fix.yml | 239 +++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 .github/workflows/apply-pr132-review-fix.yml diff --git a/.github/workflows/apply-pr132-review-fix.yml b/.github/workflows/apply-pr132-review-fix.yml new file mode 100644 index 0000000..231c272 --- /dev/null +++ b/.github/workflows/apply-pr132-review-fix.yml @@ -0,0 +1,239 @@ +name: Apply PR 132 review fix + +on: + push: + branches: + - cursor/application-security-review-6876 + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: cursor/application-security-review-6876 + fetch-depth: 0 + + - name: Apply review fix + shell: python + run: | + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected one match in {path}, found {count}") + file.write_text(text.replace(old, new, 1)) + + replace_once( + "src/session/conn.rs", + "use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track};", + "use crate::ertmp::multitrack_media::{\n first_track_fourcc, foreach_track, is_multitrack_container,\n};", + ) + + replace_once( + "src/session/conn.rs", + ''' if let Some(cb) = self.on_frame_cb { + let had_multitrack = foreach_track(frame_type, parse_payload, |track| { + self.invoke_multitrack_on_frame_cb( + cb, + frame_type, + timestamp, + track.track_id, + track.fourcc, + track.packet_type, + track.video_frame_type, + track.payload, + ); + }); + if !had_multitrack { + self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload); + } + } +''', + ''' let is_multitrack = is_multitrack_container(frame_type, parse_payload); + let cb = self.on_frame_cb; + let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| { + if let Some(cb) = cb { + self.invoke_multitrack_on_frame_cb( + cb, + frame_type, + timestamp, + track.track_id, + track.fourcc, + track.packet_type, + track.video_frame_type, + track.payload, + ); + } + }); + if is_multitrack && !parsed_multitrack { + return Err(ErrorCode::Protocol); + } + if !is_multitrack { + if let Some(cb) = cb { + self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload); + } + } +''', + ) + + replace_once( + "src/client/mod.rs", + "use crate::ertmp::multitrack_media::foreach_track;", + "use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container};", + ) + + replace_once( + "src/client/mod.rs", + " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload);", + " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?;", + ) + + replace_once( + "src/client/mod.rs", + ''' self.deliver_av_frame_cb( + cb, + FrameType::Audio, + out_ts, + tag_payload.to_vec(), + );''', + ''' self.deliver_av_frame_cb( + cb, + FrameType::Audio, + out_ts, + tag_payload.to_vec(), + )?;''', + ) + + replace_once( + "src/client/mod.rs", + ''' self.deliver_av_frame_cb( + cb, + FrameType::Video, + out_ts, + tag_payload.to_vec(), + );''', + ''' self.deliver_av_frame_cb( + cb, + FrameType::Video, + out_ts, + tag_payload.to_vec(), + )?;''', + ) + + replace_once( + "src/client/mod.rs", + ''' fn deliver_av_frame_cb( + &mut self, + cb: fn(&Frame), + frame_type: FrameType, + timestamp: u32, + payload: Vec, + ) { + let had_multitrack = foreach_track(frame_type, &payload, |track| { + self.invoke_multitrack_on_frame_cb( + cb, + frame_type, + timestamp, + track.track_id, + track.fourcc, + track.packet_type, + track.video_frame_type, + track.payload, + ); + }); + if !had_multitrack { + self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload); + } + } +''', + ''' fn deliver_av_frame_cb( + &mut self, + cb: fn(&Frame), + frame_type: FrameType, + timestamp: u32, + payload: Vec, + ) -> Result<()> { + let is_multitrack = is_multitrack_container(frame_type, &payload); + let parsed_multitrack = foreach_track(frame_type, &payload, |track| { + self.invoke_multitrack_on_frame_cb( + cb, + frame_type, + timestamp, + track.track_id, + track.fourcc, + track.packet_type, + track.video_frame_type, + track.payload, + ); + }); + if is_multitrack && !parsed_multitrack { + return Err(ErrorCode::Protocol); + } + if !is_multitrack { + self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload); + } + Ok(()) + } +''', + ) + + test = ''' + #[test] + fn drain_ready_messages_rejects_oversized_multitrack_video() { + let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1']; + for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS { + payload.push(id as u8); + payload.extend_from_slice(&[0x00, 0x00, 0x00]); + } + + let mut wire = Buffer::new(); + let mut cmsg = ChunkMessage::default(); + cmsg.csid = 6; + cmsg.fmt = 0; + cmsg.msg_length = payload.len() as u32; + cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO; + cmsg.msg_stream_id = 1; + chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap(); + + let mut client = Client::new(); + client.recv_buffer.write(wire.peek()).unwrap(); + client.on_frame_cb = Some(|_| panic!("invalid multitrack must not reach callback")); + + let mut messages_processed = 0; + assert_eq!( + client.drain_ready_messages(&mut messages_processed), + Err(ErrorCode::Protocol) + ); + assert!(client.frame_cb_scratch.is_empty()); + } + +''' + replace_once( + "src/client/mod.rs", + " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {", + test + " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {", + ) + + - name: Format + run: cargo fmt --all + + - name: Test multitrack handling + run: | + cargo test --no-default-features multitrack + cargo test --no-default-features drain_ready_messages_rejects_oversized_multitrack_video + + - name: Commit fix + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/apply-pr132-review-fix.yml + git add src/session/conn.rs src/client/mod.rs + git commit -m "Fix rejection of invalid multitrack containers" + git push origin HEAD:cursor/application-security-review-6876 From e8f48daa8fe449cc614137f888ea3b767f514e86 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:39:08 +0200 Subject: [PATCH 03/17] chore: make PR 132 patch workflow robust --- .github/workflows/apply-pr132-review-fix.yml | 289 ++++++++----------- 1 file changed, 118 insertions(+), 171 deletions(-) diff --git a/.github/workflows/apply-pr132-review-fix.yml b/.github/workflows/apply-pr132-review-fix.yml index 231c272..bc02f73 100644 --- a/.github/workflows/apply-pr132-review-fix.yml +++ b/.github/workflows/apply-pr132-review-fix.yml @@ -20,206 +20,153 @@ jobs: - name: Apply review fix shell: python run: | + import re from pathlib import Path - def replace_once(path: str, old: str, new: str) -> None: + def replace_exact(path: str, old: str, new: str, expected: int = 1) -> None: file = Path(path) text = file.read_text() count = text.count(old) + if count != expected: + raise RuntimeError(f"expected {expected} matches in {path}, found {count}: {old!r}") + file.write_text(text.replace(old, new)) + + def replace_regex(path: str, pattern: str, new: str) -> None: + file = Path(path) + text = file.read_text() + updated, count = re.subn(pattern, new, text, count=1, flags=re.DOTALL) if count != 1: - raise RuntimeError(f"expected one match in {path}, found {count}") - file.write_text(text.replace(old, new, 1)) + raise RuntimeError(f"expected one regex match in {path}, found {count}: {pattern!r}") + file.write_text(updated) - replace_once( + replace_exact( "src/session/conn.rs", "use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track};", "use crate::ertmp::multitrack_media::{\n first_track_fourcc, foreach_track, is_multitrack_container,\n};", ) - replace_once( + server_block = "\n".join([ + " let is_multitrack = is_multitrack_container(frame_type, parse_payload);", + " let cb = self.on_frame_cb;", + " let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| {", + " if let Some(cb) = cb {", + " self.invoke_multitrack_on_frame_cb(", + " cb,", + " frame_type,", + " timestamp,", + " track.track_id,", + " track.fourcc,", + " track.packet_type,", + " track.video_frame_type,", + " track.payload,", + " );", + " }", + " });", + " if is_multitrack && !parsed_multitrack {", + " return Err(ErrorCode::Protocol);", + " }", + " if !is_multitrack {", + " if let Some(cb) = cb {", + " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload);", + " }", + " }", + "", + ]) + replace_regex( "src/session/conn.rs", - ''' if let Some(cb) = self.on_frame_cb { - let had_multitrack = foreach_track(frame_type, parse_payload, |track| { - self.invoke_multitrack_on_frame_cb( - cb, - frame_type, - timestamp, - track.track_id, - track.fourcc, - track.packet_type, - track.video_frame_type, - track.payload, - ); - }); - if !had_multitrack { - self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload); - } - } -''', - ''' let is_multitrack = is_multitrack_container(frame_type, parse_payload); - let cb = self.on_frame_cb; - let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| { - if let Some(cb) = cb { - self.invoke_multitrack_on_frame_cb( - cb, - frame_type, - timestamp, - track.track_id, - track.fourcc, - track.packet_type, - track.video_frame_type, - track.payload, - ); - } - }); - if is_multitrack && !parsed_multitrack { - return Err(ErrorCode::Protocol); - } - if !is_multitrack { - if let Some(cb) = cb { - self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload); - } - } -''', + r" if let Some\(cb\) = self\.on_frame_cb \{\n let had_multitrack = foreach_track\(frame_type, parse_payload, \|track\| \{.*?\n \}\n\n if self\n", + server_block + " if self\n", ) - replace_once( + replace_exact( "src/client/mod.rs", "use crate::ertmp::multitrack_media::foreach_track;", "use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container};", ) - - replace_once( + replace_exact( "src/client/mod.rs", " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload);", " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?;", ) - - replace_once( - "src/client/mod.rs", - ''' self.deliver_av_frame_cb( - cb, - FrameType::Audio, - out_ts, - tag_payload.to_vec(), - );''', - ''' self.deliver_av_frame_cb( - cb, - FrameType::Audio, - out_ts, - tag_payload.to_vec(), - )?;''', - ) - - replace_once( + replace_exact( "src/client/mod.rs", - ''' self.deliver_av_frame_cb( - cb, - FrameType::Video, - out_ts, - tag_payload.to_vec(), - );''', - ''' self.deliver_av_frame_cb( - cb, - FrameType::Video, - out_ts, - tag_payload.to_vec(), - )?;''', + " tag_payload.to_vec(),\n );", + " tag_payload.to_vec(),\n )?;", + expected=2, ) - replace_once( + client_block = "\n".join([ + " fn deliver_av_frame_cb(", + " &mut self,", + " cb: fn(&Frame),", + " frame_type: FrameType,", + " timestamp: u32,", + " payload: Vec,", + " ) -> Result<()> {", + " let is_multitrack = is_multitrack_container(frame_type, &payload);", + " let parsed_multitrack = foreach_track(frame_type, &payload, |track| {", + " self.invoke_multitrack_on_frame_cb(", + " cb,", + " frame_type,", + " timestamp,", + " track.track_id,", + " track.fourcc,", + " track.packet_type,", + " track.video_frame_type,", + " track.payload,", + " );", + " });", + " if is_multitrack && !parsed_multitrack {", + " return Err(ErrorCode::Protocol);", + " }", + " if !is_multitrack {", + " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload);", + " }", + " Ok(())", + " }", + "", + "", + ]) + replace_regex( "src/client/mod.rs", - ''' fn deliver_av_frame_cb( - &mut self, - cb: fn(&Frame), - frame_type: FrameType, - timestamp: u32, - payload: Vec, - ) { - let had_multitrack = foreach_track(frame_type, &payload, |track| { - self.invoke_multitrack_on_frame_cb( - cb, - frame_type, - timestamp, - track.track_id, - track.fourcc, - track.packet_type, - track.video_frame_type, - track.payload, - ); - }); - if !had_multitrack { - self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload); - } - } -''', - ''' fn deliver_av_frame_cb( - &mut self, - cb: fn(&Frame), - frame_type: FrameType, - timestamp: u32, - payload: Vec, - ) -> Result<()> { - let is_multitrack = is_multitrack_container(frame_type, &payload); - let parsed_multitrack = foreach_track(frame_type, &payload, |track| { - self.invoke_multitrack_on_frame_cb( - cb, - frame_type, - timestamp, - track.track_id, - track.fourcc, - track.packet_type, - track.video_frame_type, - track.payload, - ); - }); - if is_multitrack && !parsed_multitrack { - return Err(ErrorCode::Protocol); - } - if !is_multitrack { - self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload); - } - Ok(()) - } -''', + r" fn deliver_av_frame_cb\(.*?\n fn invoke_multitrack_on_frame_cb\(", + client_block + " fn invoke_multitrack_on_frame_cb(", ) - test = ''' - #[test] - fn drain_ready_messages_rejects_oversized_multitrack_video() { - let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1']; - for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS { - payload.push(id as u8); - payload.extend_from_slice(&[0x00, 0x00, 0x00]); - } - - let mut wire = Buffer::new(); - let mut cmsg = ChunkMessage::default(); - cmsg.csid = 6; - cmsg.fmt = 0; - cmsg.msg_length = payload.len() as u32; - cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO; - cmsg.msg_stream_id = 1; - chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap(); - - let mut client = Client::new(); - client.recv_buffer.write(wire.peek()).unwrap(); - client.on_frame_cb = Some(|_| panic!("invalid multitrack must not reach callback")); - - let mut messages_processed = 0; - assert_eq!( - client.drain_ready_messages(&mut messages_processed), - Err(ErrorCode::Protocol) - ); - assert!(client.frame_cb_scratch.is_empty()); - } - -''' - replace_once( - "src/client/mod.rs", - " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {", - test + " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {", - ) + test_lines = [ + " #[test]", + " fn drain_ready_messages_rejects_oversized_multitrack_video() {", + " let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1'];", + " for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS {", + " payload.push(id as u8);", + " payload.extend_from_slice(&[0x00, 0x00, 0x00]);", + " }", + "", + " let mut wire = Buffer::new();", + " let mut cmsg = ChunkMessage::default();", + " cmsg.csid = 6;", + " cmsg.fmt = 0;", + " cmsg.msg_length = payload.len() as u32;", + " cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO;", + " cmsg.msg_stream_id = 1;", + " chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap();", + "", + " let mut client = Client::new();", + " client.recv_buffer.write(wire.peek()).unwrap();", + " client.on_frame_cb = Some(|_| panic!(\"invalid multitrack must not reach callback\"));", + "", + " let mut messages_processed = 0;", + " assert_eq!(", + " client.drain_ready_messages(&mut messages_processed),", + " Err(ErrorCode::Protocol)", + " );", + " assert!(client.frame_cb_scratch.is_empty());", + " }", + "", + "", + ] + marker = " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {" + replace_exact("src/client/mod.rs", marker, "\n".join(test_lines) + marker) - name: Format run: cargo fmt --all From 92884df6729196473abe453a9cfbbe0f9645af83 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:43:57 +0200 Subject: [PATCH 04/17] ci: trigger PR 132 review fix --- .github/pr132-review-fix.trigger | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/pr132-review-fix.trigger diff --git a/.github/pr132-review-fix.trigger b/.github/pr132-review-fix.trigger new file mode 100644 index 0000000..854cb87 --- /dev/null +++ b/.github/pr132-review-fix.trigger @@ -0,0 +1 @@ +Apply the PR 132 review fix. From 36ef553f571eeb75d99b8c1905ba1822da8ecbd9 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:46:37 +0200 Subject: [PATCH 05/17] ci: run PR 132 fix on synchronization --- .github/workflows/apply-pr132-review-fix.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/apply-pr132-review-fix.yml b/.github/workflows/apply-pr132-review-fix.yml index bc02f73..7366efb 100644 --- a/.github/workflows/apply-pr132-review-fix.yml +++ b/.github/workflows/apply-pr132-review-fix.yml @@ -4,6 +4,11 @@ on: push: branches: - cursor/application-security-review-6876 + pull_request: + branches: + - main + types: + - synchronize permissions: contents: write @@ -181,6 +186,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git rm .github/workflows/apply-pr132-review-fix.yml + git rm .github/pr132-review-fix.trigger git add src/session/conn.rs src/client/mod.rs git commit -m "Fix rejection of invalid multitrack containers" git push origin HEAD:cursor/application-security-review-6876 From 70dbdafa12c289f7b66b00299c63f9c1b62e49c2 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:51:07 +0200 Subject: [PATCH 06/17] ci: prepare PR 132 review patch --- .github/scripts/pr132_review_fix.py | 170 ++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/scripts/pr132_review_fix.py diff --git a/.github/scripts/pr132_review_fix.py b/.github/scripts/pr132_review_fix.py new file mode 100644 index 0000000..f3119fd --- /dev/null +++ b/.github/scripts/pr132_review_fix.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import re +from pathlib import Path + + +def replace_exact(path: str, old: str, new: str, expected: int = 1) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != expected: + raise RuntimeError( + f"expected {expected} matches in {path}, found {count}: {old!r}" + ) + file.write_text(text.replace(old, new)) + + +def replace_regex(path: str, pattern: str, new: str) -> None: + file = Path(path) + text = file.read_text() + updated, count = re.subn(pattern, new, text, count=1, flags=re.DOTALL) + if count != 1: + raise RuntimeError( + f"expected one regex match in {path}, found {count}: {pattern!r}" + ) + file.write_text(updated) + + +replace_exact( + "src/session/conn.rs", + "use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track};", + "use crate::ertmp::multitrack_media::{\n" + " first_track_fourcc, foreach_track, is_multitrack_container,\n" + "};", +) + +server_block = "\n".join( + [ + " let is_multitrack = is_multitrack_container(frame_type, parse_payload);", + " let cb = self.on_frame_cb;", + " let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| {", + " if let Some(cb) = cb {", + " self.invoke_multitrack_on_frame_cb(", + " cb,", + " frame_type,", + " timestamp,", + " track.track_id,", + " track.fourcc,", + " track.packet_type,", + " track.video_frame_type,", + " track.payload,", + " );", + " }", + " });", + " if is_multitrack && !parsed_multitrack {", + " return Err(ErrorCode::Protocol);", + " }", + " if !is_multitrack {", + " if let Some(cb) = cb {", + " self.invoke_on_frame_cb(", + " cb,", + " frame_type,", + " timestamp,", + " u8::MAX,", + " parse_payload,", + " );", + " }", + " }", + "", + ] +) +replace_regex( + "src/session/conn.rs", + r" if let Some\(cb\) = self\.on_frame_cb \{\n" + r" let had_multitrack = foreach_track\(frame_type, parse_payload, \|track\| \{" + r".*?\n \}\n\n if self\n", + server_block + " if self\n", +) + +replace_exact( + "src/client/mod.rs", + "use crate::ertmp::multitrack_media::foreach_track;", + "use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container};", +) +replace_exact( + "src/client/mod.rs", + " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload);", + " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?;", +) +replace_exact( + "src/client/mod.rs", + " tag_payload.to_vec(),\n );", + " tag_payload.to_vec(),\n )?;", + expected=2, +) + +client_block = "\n".join( + [ + " fn deliver_av_frame_cb(", + " &mut self,", + " cb: fn(&Frame),", + " frame_type: FrameType,", + " timestamp: u32,", + " payload: Vec,", + " ) -> Result<()> {", + " let is_multitrack = is_multitrack_container(frame_type, &payload);", + " let parsed_multitrack = foreach_track(frame_type, &payload, |track| {", + " self.invoke_multitrack_on_frame_cb(", + " cb,", + " frame_type,", + " timestamp,", + " track.track_id,", + " track.fourcc,", + " track.packet_type,", + " track.video_frame_type,", + " track.payload,", + " );", + " });", + " if is_multitrack && !parsed_multitrack {", + " return Err(ErrorCode::Protocol);", + " }", + " if !is_multitrack {", + " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload);", + " }", + " Ok(())", + " }", + "", + "", + ] +) +replace_regex( + "src/client/mod.rs", + r" fn deliver_av_frame_cb\(.*?\n fn invoke_multitrack_on_frame_cb\(", + client_block + " fn invoke_multitrack_on_frame_cb(", +) + +test_lines = [ + " #[test]", + " fn drain_ready_messages_rejects_oversized_multitrack_video() {", + " let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1'];", + " for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS {", + " payload.push(id as u8);", + " payload.extend_from_slice(&[0x00, 0x00, 0x00]);", + " }", + "", + " let mut wire = Buffer::new();", + " let mut cmsg = ChunkMessage::default();", + " cmsg.csid = 6;", + " cmsg.fmt = 0;", + " cmsg.msg_length = payload.len() as u32;", + " cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO;", + " cmsg.msg_stream_id = 1;", + " chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap();", + "", + " let mut client = Client::new();", + " client.recv_buffer.write(wire.peek()).unwrap();", + " client.on_frame_cb = Some(|_| panic!(\"invalid multitrack must not reach callback\"));", + "", + " let mut messages_processed = 0;", + " assert_eq!(", + " client.drain_ready_messages(&mut messages_processed),", + " Err(ErrorCode::Protocol)", + " );", + " assert!(client.frame_cb_scratch.is_empty());", + " }", + "", + "", +] +marker = " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {" +replace_exact("src/client/mod.rs", marker, "\n".join(test_lines) + marker) From c76b07763d2522649705c90a9ebe207f8d071bfd Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:51:23 +0200 Subject: [PATCH 07/17] ci: apply and validate PR 132 review fix --- .github/workflows/tests.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bf1d73..b75371d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write jobs: test: @@ -15,7 +15,11 @@ jobs: steps: - uses: actions/checkout@v7 with: - persist-credentials: false + ref: cursor/application-security-review-6876 + fetch-depth: 0 + + - name: Apply PR 132 review fix + run: python3 .github/scripts/pr132_review_fix.py - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config @@ -25,6 +29,9 @@ jobs: - uses: Swatinem/rust-cache@v2 + - name: Format + run: cargo fmt --all -- --check + - name: Build (default features) run: cargo build --all-features --verbose @@ -48,3 +55,15 @@ jobs: - name: Clippy run: cargo clippy --all-features --all-targets + + - name: Commit validated fix + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout 99a23475c4d58bd3c64dd86fcbd3bb121b5226be -- .github/workflows/tests.yml + git rm .github/workflows/apply-pr132-review-fix.yml + git rm .github/pr132-review-fix.trigger + git rm .github/scripts/pr132_review_fix.py + git add src/session/conn.rs src/client/mod.rs .github/workflows/tests.yml + git commit -m "Fix rejection of invalid multitrack containers" + git push origin HEAD:cursor/application-security-review-6876 From ce27bb992712e56bfd312ea122c5b2ff9dfb6e89 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:54:38 +0200 Subject: [PATCH 08/17] ci: capture PR 132 patch diagnostics --- .github/workflows/apply-pr132-review-fix.yml | 174 ++----------------- 1 file changed, 13 insertions(+), 161 deletions(-) diff --git a/.github/workflows/apply-pr132-review-fix.yml b/.github/workflows/apply-pr132-review-fix.yml index 7366efb..94e8bd5 100644 --- a/.github/workflows/apply-pr132-review-fix.yml +++ b/.github/workflows/apply-pr132-review-fix.yml @@ -1,9 +1,6 @@ name: Apply PR 132 review fix on: - push: - branches: - - cursor/application-security-review-6876 pull_request: branches: - main @@ -23,170 +20,25 @@ jobs: fetch-depth: 0 - name: Apply review fix - shell: python - run: | - import re - from pathlib import Path - - def replace_exact(path: str, old: str, new: str, expected: int = 1) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != expected: - raise RuntimeError(f"expected {expected} matches in {path}, found {count}: {old!r}") - file.write_text(text.replace(old, new)) - - def replace_regex(path: str, pattern: str, new: str) -> None: - file = Path(path) - text = file.read_text() - updated, count = re.subn(pattern, new, text, count=1, flags=re.DOTALL) - if count != 1: - raise RuntimeError(f"expected one regex match in {path}, found {count}: {pattern!r}") - file.write_text(updated) - - replace_exact( - "src/session/conn.rs", - "use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track};", - "use crate::ertmp::multitrack_media::{\n first_track_fourcc, foreach_track, is_multitrack_container,\n};", - ) - - server_block = "\n".join([ - " let is_multitrack = is_multitrack_container(frame_type, parse_payload);", - " let cb = self.on_frame_cb;", - " let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| {", - " if let Some(cb) = cb {", - " self.invoke_multitrack_on_frame_cb(", - " cb,", - " frame_type,", - " timestamp,", - " track.track_id,", - " track.fourcc,", - " track.packet_type,", - " track.video_frame_type,", - " track.payload,", - " );", - " }", - " });", - " if is_multitrack && !parsed_multitrack {", - " return Err(ErrorCode::Protocol);", - " }", - " if !is_multitrack {", - " if let Some(cb) = cb {", - " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload);", - " }", - " }", - "", - ]) - replace_regex( - "src/session/conn.rs", - r" if let Some\(cb\) = self\.on_frame_cb \{\n let had_multitrack = foreach_track\(frame_type, parse_payload, \|track\| \{.*?\n \}\n\n if self\n", - server_block + " if self\n", - ) - - replace_exact( - "src/client/mod.rs", - "use crate::ertmp::multitrack_media::foreach_track;", - "use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container};", - ) - replace_exact( - "src/client/mod.rs", - " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload);", - " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?;", - ) - replace_exact( - "src/client/mod.rs", - " tag_payload.to_vec(),\n );", - " tag_payload.to_vec(),\n )?;", - expected=2, - ) - - client_block = "\n".join([ - " fn deliver_av_frame_cb(", - " &mut self,", - " cb: fn(&Frame),", - " frame_type: FrameType,", - " timestamp: u32,", - " payload: Vec,", - " ) -> Result<()> {", - " let is_multitrack = is_multitrack_container(frame_type, &payload);", - " let parsed_multitrack = foreach_track(frame_type, &payload, |track| {", - " self.invoke_multitrack_on_frame_cb(", - " cb,", - " frame_type,", - " timestamp,", - " track.track_id,", - " track.fourcc,", - " track.packet_type,", - " track.video_frame_type,", - " track.payload,", - " );", - " });", - " if is_multitrack && !parsed_multitrack {", - " return Err(ErrorCode::Protocol);", - " }", - " if !is_multitrack {", - " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload);", - " }", - " Ok(())", - " }", - "", - "", - ]) - replace_regex( - "src/client/mod.rs", - r" fn deliver_av_frame_cb\(.*?\n fn invoke_multitrack_on_frame_cb\(", - client_block + " fn invoke_multitrack_on_frame_cb(", - ) - - test_lines = [ - " #[test]", - " fn drain_ready_messages_rejects_oversized_multitrack_video() {", - " let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1'];", - " for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS {", - " payload.push(id as u8);", - " payload.extend_from_slice(&[0x00, 0x00, 0x00]);", - " }", - "", - " let mut wire = Buffer::new();", - " let mut cmsg = ChunkMessage::default();", - " cmsg.csid = 6;", - " cmsg.fmt = 0;", - " cmsg.msg_length = payload.len() as u32;", - " cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO;", - " cmsg.msg_stream_id = 1;", - " chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap();", - "", - " let mut client = Client::new();", - " client.recv_buffer.write(wire.peek()).unwrap();", - " client.on_frame_cb = Some(|_| panic!(\"invalid multitrack must not reach callback\"));", - "", - " let mut messages_processed = 0;", - " assert_eq!(", - " client.drain_ready_messages(&mut messages_processed),", - " Err(ErrorCode::Protocol)", - " );", - " assert!(client.frame_cb_scratch.is_empty());", - " }", - "", - "", - ] - marker = " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {" - replace_exact("src/client/mod.rs", marker, "\n".join(test_lines) + marker) + run: python3 .github/scripts/pr132_review_fix.py - name: Format run: cargo fmt --all - - name: Test multitrack handling + - name: Capture test result run: | - cargo test --no-default-features multitrack - cargo test --no-default-features drain_ready_messages_rejects_oversized_multitrack_video - - - name: Commit fix + set +e + cargo test --no-default-features multitrack > .github/pr132-test.log 2>&1 + first_status=$? + cargo test --no-default-features drain_ready_messages_rejects_oversized_multitrack_video >> .github/pr132-test.log 2>&1 + second_status=$? + printf '%s %s\n' "$first_status" "$second_status" > .github/pr132-test.status + exit 0 + + - name: Commit diagnostics run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm .github/workflows/apply-pr132-review-fix.yml - git rm .github/pr132-review-fix.trigger - git add src/session/conn.rs src/client/mod.rs - git commit -m "Fix rejection of invalid multitrack containers" + git add src/session/conn.rs src/client/mod.rs .github/pr132-test.log .github/pr132-test.status + git commit -m "Apply multitrack rejection fix for validation" git push origin HEAD:cursor/application-security-review-6876 From acbf55dc5b3c03d3aec000513205c030a6d27232 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:56:26 +0000 Subject: [PATCH 09/17] Apply multitrack rejection fix for validation --- .github/pr132-test.log | 188 ++++++++++++++++++++++++++++++++++++++ .github/pr132-test.status | 1 + src/client/mod.rs | 48 ++++++++-- src/session/conn.rs | 18 ++-- 4 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 .github/pr132-test.log create mode 100644 .github/pr132-test.status diff --git a/.github/pr132-test.log b/.github/pr132-test.log new file mode 100644 index 0000000..614d5f4 --- /dev/null +++ b/.github/pr132-test.log @@ -0,0 +1,188 @@ + Updating crates.io index + Locking 94 packages to latest compatible versions + Downloading crates ... + Downloaded anstyle v1.0.14 + Downloaded autocfg v1.5.1 + Downloaded anes v0.1.6 + Downloaded serde_core v1.0.228 + Downloaded aho-corasick v1.1.4 + Downloaded cfg-if v1.0.4 + Downloaded crossbeam-epoch v0.9.20 + Downloaded find-msvc-tools v0.1.9 + Downloaded ciborium-ll v0.2.2 + Downloaded clap v4.6.2 + Downloaded either v1.16.0 + Downloaded cc v1.2.67 + Downloaded ciborium v0.2.2 + Downloaded scopeguard v1.2.0 + Downloaded alloca v0.4.0 + Downloaded clap_lex v1.1.0 + Downloaded criterion-plot v0.8.2 + Downloaded serde_derive v1.0.228 + Downloaded itertools v0.13.0 + Downloaded crossbeam-deque v0.8.7 + Downloaded same-file v1.0.6 + Downloaded serde v1.0.228 + Downloaded memchr v2.8.3 + Downloaded zmij v1.0.23 + Downloaded cast v0.3.0 + Downloaded thiserror v2.0.18 + Downloaded page_size v0.6.0 + Downloaded plotters-backend v0.3.7 + Downloaded tinytemplate v1.2.1 + Downloaded walkdir v2.5.0 + Downloaded quote v1.0.46 + Downloaded oorandom v11.1.5 + Downloaded ciborium-io v0.2.2 + Downloaded itoa v1.0.18 + Downloaded parking_lot v0.12.5 + Downloaded proc-macro2 v1.0.106 + Downloaded crossbeam-utils v0.8.22 + Downloaded lock_api v0.4.14 + Downloaded plotters v0.3.7 + Downloaded serde_json v1.0.150 + Downloaded shlex v2.0.1 + Downloaded half v2.7.1 + Downloaded num-traits v0.2.19 + Downloaded rayon-core v1.13.0 + Downloaded clap_builder v4.6.2 + Downloaded plotters-svg v0.3.7 + Downloaded regex v1.13.1 + Downloaded unicode-ident v1.0.24 + Downloaded criterion v0.8.2 + Downloaded thiserror-impl v2.0.18 + Downloaded libc v0.2.186 + Downloaded parking_lot_core v0.9.12 + Downloaded smallvec v1.15.2 + Downloaded zerocopy v0.8.54 + Downloaded zerocopy-derive v0.8.54 + Downloaded regex-syntax v0.8.11 + Downloaded rayon v1.12.0 + Downloaded syn v2.0.119 + Downloaded regex-automata v0.4.16 + Compiling proc-macro2 v1.0.106 + Compiling unicode-ident v1.0.24 + Compiling quote v1.0.46 + Compiling find-msvc-tools v0.1.9 + Compiling shlex v2.0.1 + Compiling cc v1.2.67 + Compiling serde_core v1.0.228 + Compiling cfg-if v1.0.4 + Compiling libc v0.2.186 + Compiling crossbeam-utils v0.8.22 + Compiling zerocopy v0.8.54 + Compiling crossbeam-epoch v0.9.20 + Compiling zmij v1.0.23 + Compiling crossbeam-deque v0.8.7 + Compiling syn v2.0.119 + Compiling serde v1.0.228 + Compiling autocfg v1.5.1 + Compiling parking_lot_core v0.9.12 + Compiling num-traits v0.2.19 + Compiling serde_json v1.0.150 + Compiling rayon-core v1.13.0 + Compiling either v1.16.0 + Compiling alloca v0.4.0 + Compiling clap_lex v1.1.0 + Compiling anstyle v1.0.14 + Compiling itoa v1.0.18 + Compiling plotters-backend v0.3.7 + Compiling thiserror v2.0.18 + Compiling smallvec v1.15.2 + Compiling regex-syntax v0.8.11 + Compiling memchr v2.8.3 + Compiling scopeguard v1.2.0 + Compiling ciborium-io v0.2.2 + Compiling lock_api v0.4.14 + Compiling plotters-svg v0.3.7 + Compiling clap_builder v4.6.2 + Compiling regex-automata v0.4.16 + Compiling zerocopy-derive v0.8.54 + Compiling serde_derive v1.0.228 + Compiling thiserror-impl v2.0.18 + Compiling itertools v0.13.0 + Compiling librtmp2 v0.4.0 (/home/runner/work/librtmp2/librtmp2) + Compiling cast v0.3.0 + Compiling same-file v1.0.6 + Compiling criterion-plot v0.8.2 + Compiling walkdir v2.5.0 + Compiling half v2.7.1 + Compiling plotters v0.3.7 + Compiling ciborium-ll v0.2.2 + Compiling rayon v1.12.0 + Compiling ciborium v0.2.2 + Compiling tinytemplate v1.2.1 + Compiling regex v1.13.1 + Compiling clap v4.6.2 + Compiling parking_lot v0.12.5 + Compiling page_size v0.6.0 + Compiling anes v0.1.6 + Compiling oorandom v11.1.5 + Compiling criterion v0.8.2 + Finished `test` profile [unoptimized + debuginfo] target(s) in 27.35s + Running unittests src/lib.rs (target/debug/deps/librtmp2-ff643fd5d13d1ade) + +running 22 tests +test client::tests::drain_ready_messages_splits_multitrack_video ... ok +test ertmp::multitrack::tests::audio_track_type_round_trips ... ok +test ertmp::multitrack::tests::parse_error_leaves_descriptor_cleared ... ok +test ertmp::multitrack::tests::video_track_type_round_trips_as_amf0_double ... ok +test ertmp::multitrack_media::tests::coded_frames_x_keyframe_is_detected ... ok +test ertmp::multitrack_media::tests::detects_multitrack_container ... ok +test ertmp::multitrack_media::tests::iterates_many_tracks_many_codecs ... ok +test ertmp::multitrack_media::tests::iterates_subtracks_with_shared_codec ... ok +test ertmp::multitrack_media::tests::malformed_message_delivers_no_partial_tracks ... ok +test media::init_cache::tests::multitrack_audio_callback_metadata_uses_track_descriptor ... ok +test media::init_cache::tests::multitrack_video_callback_metadata_uses_track_descriptor ... ok +test media::init_cache::tests::multitrack_video_keyframe_is_cached_from_outer_header ... ok +test media::init_cache::tests::multitrack_video_sequence_start_is_cached ... ok +test server::tests::modex_wrapped_multitrack_is_detected_for_player_gating ... ok +test server::tests::multitrack_per_track_video_inits_are_retained ... ok +test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... FAILED +test server::tests::multitrack_video_sequence_header_is_cached ... ok +test session::conn::tests::multitrack_codec_is_detected_before_authorization ... ok +test session::conn::tests::connect_with_caps_ex_enables_multitrack ... ok +test session::conn::tests::multitrack_invokes_on_frame_cb_per_track ... ok +test session::conn::tests::multitrack_on_frame_cb_scratch_retains_last_track_payload ... ok +test ertmp::multitrack_media::tests::rejects_multitrack_messages_with_too_many_subtracks ... ok + +failures: + +---- client::tests::drain_ready_messages_rejects_oversized_multitrack_video stdout ---- + +thread 'client::tests::drain_ready_messages_rejects_oversized_multitrack_video' (4178) panicked at src/client/mod.rs:1351:9: +assertion `left == right` failed + left: Ok(()) + right: Err(Protocol) +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + client::tests::drain_ready_messages_rejects_oversized_multitrack_video + +test result: FAILED. 21 passed; 1 failed; 0 ignored; 0 measured; 238 filtered out; finished in 0.00s + +error: test failed, to rerun pass `--lib` + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s + Running unittests src/lib.rs (target/debug/deps/librtmp2-ff643fd5d13d1ade) + +running 1 test +test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... FAILED + +failures: + +---- client::tests::drain_ready_messages_rejects_oversized_multitrack_video stdout ---- + +thread 'client::tests::drain_ready_messages_rejects_oversized_multitrack_video' (4208) panicked at src/client/mod.rs:1351:9: +assertion `left == right` failed + left: Ok(()) + right: Err(Protocol) +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + client::tests::drain_ready_messages_rejects_oversized_multitrack_video + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 259 filtered out; finished in 0.00s + +error: test failed, to rerun pass `--lib` diff --git a/.github/pr132-test.status b/.github/pr132-test.status new file mode 100644 index 0000000..6432555 --- /dev/null +++ b/.github/pr132-test.status @@ -0,0 +1 @@ +101 101 diff --git a/src/client/mod.rs b/src/client/mod.rs index 427b350..baad796 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -11,7 +11,7 @@ use crate::buffer::Buffer; use crate::chunk::reader::{ChunkMessage, chunk_read_owned}; use crate::chunk::state::{ChunkRegistry, DEFAULT_MAX_MSG_LENGTH}; use crate::chunk::writer::chunk_write; -use crate::ertmp::multitrack_media::foreach_track; +use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container}; use crate::handshake::{self, Handshake}; use crate::media::{is_on_metadata_payload, populate_av_frame, populate_multitrack_frame}; use crate::message::command; @@ -526,7 +526,7 @@ impl Client { } else { FrameType::Video }; - self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload); + self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?; } } else if msg.msg_type_id == msg_dispatch::RTMP_MSG_AMF0_DATA || msg.msg_type_id == msg_dispatch::RTMP_MSG_AMF3_DATA @@ -594,7 +594,7 @@ impl Client { FrameType::Audio, out_ts, tag_payload.to_vec(), - ); + )?; } msg_dispatch::RTMP_MSG_VIDEO => { self.deliver_av_frame_cb( @@ -602,7 +602,7 @@ impl Client { FrameType::Video, out_ts, tag_payload.to_vec(), - ); + )?; } msg_dispatch::RTMP_MSG_AMF0_DATA => { self.deliver_script_frame_cb(cb, out_ts, tag_payload); @@ -627,8 +627,9 @@ impl Client { frame_type: FrameType, timestamp: u32, payload: Vec, - ) { - let had_multitrack = foreach_track(frame_type, &payload, |track| { + ) -> Result<()> { + let is_multitrack = is_multitrack_container(frame_type, &payload); + let parsed_multitrack = foreach_track(frame_type, &payload, |track| { self.invoke_multitrack_on_frame_cb( cb, frame_type, @@ -640,9 +641,13 @@ impl Client { track.payload, ); }); - if !had_multitrack { + if is_multitrack && !parsed_multitrack { + return Err(ErrorCode::Protocol); + } + if !is_multitrack { self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload); } + Ok(()) } fn invoke_multitrack_on_frame_cb( @@ -1321,6 +1326,35 @@ mod tests { assert_eq!(seen[1].1, vec![0xDD, 0xEE]); } + #[test] + fn drain_ready_messages_rejects_oversized_multitrack_video() { + let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1']; + for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS { + payload.push(id as u8); + payload.extend_from_slice(&[0x00, 0x00, 0x00]); + } + + let mut wire = Buffer::new(); + let mut cmsg = ChunkMessage::default(); + cmsg.csid = 6; + cmsg.fmt = 0; + cmsg.msg_length = payload.len() as u32; + cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO; + cmsg.msg_stream_id = 1; + chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap(); + + let mut client = Client::new(); + client.recv_buffer.write(wire.peek()).unwrap(); + client.on_frame_cb = Some(|_| panic!("invalid multitrack must not reach callback")); + + let mut messages_processed = 0; + assert_eq!( + client.drain_ready_messages(&mut messages_processed), + Err(ErrorCode::Protocol) + ); + assert!(client.frame_cb_scratch.is_empty()); + } + #[test] fn poll_drains_leftover_messages_before_enforcing_staging_cap() { use std::io::Write; diff --git a/src/session/conn.rs b/src/session/conn.rs index ebf007b..8af81c8 100644 --- a/src/session/conn.rs +++ b/src/session/conn.rs @@ -7,7 +7,7 @@ use crate::chunk::reader::{ChunkMessage, chunk_read_owned}; use crate::chunk::state::{ChunkRegistry, DEFAULT_CHUNK_SIZE, DEFAULT_MAX_MSG_LENGTH}; use crate::chunk::writer::chunk_write; use crate::ertmp::connect_amf::{negotiate_caps, write_negotiated_caps}; -use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track}; +use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track, is_multitrack_container}; use crate::handshake::{self, Handshake, HandshakeState}; use crate::media::{ is_on_metadata_payload, normalize_modex_payload, populate_av_frame, populate_multitrack_frame, @@ -471,8 +471,10 @@ impl Conn { .media_bytes_received .saturating_add(payload.len() as u64); - if let Some(cb) = self.on_frame_cb { - let had_multitrack = foreach_track(frame_type, parse_payload, |track| { + let is_multitrack = is_multitrack_container(frame_type, parse_payload); + let cb = self.on_frame_cb; + let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| { + if let Some(cb) = cb { self.invoke_multitrack_on_frame_cb( cb, frame_type, @@ -483,12 +485,16 @@ impl Conn { track.video_frame_type, track.payload, ); - }); - if !had_multitrack { + } + }); + if is_multitrack && !parsed_multitrack { + return Err(ErrorCode::Protocol); + } + if !is_multitrack { + if let Some(cb) = cb { self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, parse_payload); } } - if self .queue_relay_frame(frame_type, timestamp, payload, parse_payload) .is_err() From b359cda2b11d4240464efcc753ababd24b04e0ac Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:58:21 +0200 Subject: [PATCH 10/17] ci: restore standard tests workflow --- .github/workflows/tests.yml | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b75371d..0bf1d73 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: test: @@ -15,11 +15,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: cursor/application-security-review-6876 - fetch-depth: 0 - - - name: Apply PR 132 review fix - run: python3 .github/scripts/pr132_review_fix.py + persist-credentials: false - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config @@ -29,9 +25,6 @@ jobs: - uses: Swatinem/rust-cache@v2 - - name: Format - run: cargo fmt --all -- --check - - name: Build (default features) run: cargo build --all-features --verbose @@ -55,15 +48,3 @@ jobs: - name: Clippy run: cargo clippy --all-features --all-targets - - - name: Commit validated fix - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout 99a23475c4d58bd3c64dd86fcbd3bb121b5226be -- .github/workflows/tests.yml - git rm .github/workflows/apply-pr132-review-fix.yml - git rm .github/pr132-review-fix.trigger - git rm .github/scripts/pr132_review_fix.py - git add src/session/conn.rs src/client/mod.rs .github/workflows/tests.yml - git commit -m "Fix rejection of invalid multitrack containers" - git push origin HEAD:cursor/application-security-review-6876 From 54f9ea22dd7eda4d858d529680b2148972bc0e04 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 13:58:33 +0200 Subject: [PATCH 11/17] test: fully reassemble oversized multitrack message --- .github/scripts/pr132_review_fix.py | 185 +++------------------------- 1 file changed, 20 insertions(+), 165 deletions(-) diff --git a/.github/scripts/pr132_review_fix.py b/.github/scripts/pr132_review_fix.py index f3119fd..8fff494 100644 --- a/.github/scripts/pr132_review_fix.py +++ b/.github/scripts/pr132_review_fix.py @@ -3,168 +3,23 @@ import re from pathlib import Path - -def replace_exact(path: str, old: str, new: str, expected: int = 1) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != expected: - raise RuntimeError( - f"expected {expected} matches in {path}, found {count}: {old!r}" - ) - file.write_text(text.replace(old, new)) - - -def replace_regex(path: str, pattern: str, new: str) -> None: - file = Path(path) - text = file.read_text() - updated, count = re.subn(pattern, new, text, count=1, flags=re.DOTALL) - if count != 1: - raise RuntimeError( - f"expected one regex match in {path}, found {count}: {pattern!r}" - ) - file.write_text(updated) - - -replace_exact( - "src/session/conn.rs", - "use crate::ertmp::multitrack_media::{first_track_fourcc, foreach_track};", - "use crate::ertmp::multitrack_media::{\n" - " first_track_fourcc, foreach_track, is_multitrack_container,\n" - "};", -) - -server_block = "\n".join( - [ - " let is_multitrack = is_multitrack_container(frame_type, parse_payload);", - " let cb = self.on_frame_cb;", - " let parsed_multitrack = foreach_track(frame_type, parse_payload, |track| {", - " if let Some(cb) = cb {", - " self.invoke_multitrack_on_frame_cb(", - " cb,", - " frame_type,", - " timestamp,", - " track.track_id,", - " track.fourcc,", - " track.packet_type,", - " track.video_frame_type,", - " track.payload,", - " );", - " }", - " });", - " if is_multitrack && !parsed_multitrack {", - " return Err(ErrorCode::Protocol);", - " }", - " if !is_multitrack {", - " if let Some(cb) = cb {", - " self.invoke_on_frame_cb(", - " cb,", - " frame_type,", - " timestamp,", - " u8::MAX,", - " parse_payload,", - " );", - " }", - " }", - "", - ] -) -replace_regex( - "src/session/conn.rs", - r" if let Some\(cb\) = self\.on_frame_cb \{\n" - r" let had_multitrack = foreach_track\(frame_type, parse_payload, \|track\| \{" - r".*?\n \}\n\n if self\n", - server_block + " if self\n", -) - -replace_exact( - "src/client/mod.rs", - "use crate::ertmp::multitrack_media::foreach_track;", - "use crate::ertmp::multitrack_media::{foreach_track, is_multitrack_container};", -) -replace_exact( - "src/client/mod.rs", - " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload);", - " self.deliver_av_frame_cb(cb, frame_type, msg.timestamp, payload)?;", -) -replace_exact( - "src/client/mod.rs", - " tag_payload.to_vec(),\n );", - " tag_payload.to_vec(),\n )?;", - expected=2, -) - -client_block = "\n".join( - [ - " fn deliver_av_frame_cb(", - " &mut self,", - " cb: fn(&Frame),", - " frame_type: FrameType,", - " timestamp: u32,", - " payload: Vec,", - " ) -> Result<()> {", - " let is_multitrack = is_multitrack_container(frame_type, &payload);", - " let parsed_multitrack = foreach_track(frame_type, &payload, |track| {", - " self.invoke_multitrack_on_frame_cb(", - " cb,", - " frame_type,", - " timestamp,", - " track.track_id,", - " track.fourcc,", - " track.packet_type,", - " track.video_frame_type,", - " track.payload,", - " );", - " });", - " if is_multitrack && !parsed_multitrack {", - " return Err(ErrorCode::Protocol);", - " }", - " if !is_multitrack {", - " self.invoke_on_frame_cb(cb, frame_type, timestamp, u8::MAX, &payload);", - " }", - " Ok(())", - " }", - "", - "", - ] -) -replace_regex( - "src/client/mod.rs", - r" fn deliver_av_frame_cb\(.*?\n fn invoke_multitrack_on_frame_cb\(", - client_block + " fn invoke_multitrack_on_frame_cb(", -) - -test_lines = [ - " #[test]", - " fn drain_ready_messages_rejects_oversized_multitrack_video() {", - " let mut payload = vec![0x86, 0x10, b'a', b'v', b'c', b'1'];", - " for id in 0..=crate::ertmp::multitrack_media::MAX_MULTITRACK_SUBTRACKS {", - " payload.push(id as u8);", - " payload.extend_from_slice(&[0x00, 0x00, 0x00]);", - " }", - "", - " let mut wire = Buffer::new();", - " let mut cmsg = ChunkMessage::default();", - " cmsg.csid = 6;", - " cmsg.fmt = 0;", - " cmsg.msg_length = payload.len() as u32;", - " cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO;", - " cmsg.msg_stream_id = 1;", - " chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap();", - "", - " let mut client = Client::new();", - " client.recv_buffer.write(wire.peek()).unwrap();", - " client.on_frame_cb = Some(|_| panic!(\"invalid multitrack must not reach callback\"));", - "", - " let mut messages_processed = 0;", - " assert_eq!(", - " client.drain_ready_messages(&mut messages_processed),", - " Err(ErrorCode::Protocol)", - " );", - " assert!(client.frame_cb_scratch.is_empty());", - " }", - "", - "", -] -marker = " #[test]\n fn poll_drains_leftover_messages_before_enforcing_staging_cap() {" -replace_exact("src/client/mod.rs", marker, "\n".join(test_lines) + marker) +path = Path("src/client/mod.rs") +text = path.read_text() +pattern = re.compile( + r"( fn drain_ready_messages_rejects_oversized_multitrack_video\(\) \{" + r".*? cmsg\.msg_stream_id = 1;\n)" + r" chunk_write\(&mut wire, &cmsg, &payload, payload\.len\(\), 128\)\.unwrap\(\);\n\n" + r" let mut client = Client::new\(\);\n", + re.DOTALL, +) +replacement = ( + r"\1" + " let chunk_size = payload.len();\n" + " chunk_write(&mut wire, &cmsg, &payload, payload.len(), chunk_size).unwrap();\n\n" + " let mut client = Client::new();\n" + " client.chunk_reg.set_all_chunk_size(chunk_size as u32);\n" +) +updated, count = pattern.subn(replacement, text, count=1) +if count != 1: + raise RuntimeError(f"expected one oversized-multitrack test block, found {count}") +path.write_text(updated) From d2535318082e29b5c309178ba97966a0630bd41c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:00:16 +0000 Subject: [PATCH 12/17] Apply multitrack rejection fix for validation --- .github/pr132-test.log | 230 ++++++++++++++++++++++---------------- .github/pr132-test.status | 2 +- src/client/mod.rs | 4 +- 3 files changed, 140 insertions(+), 96 deletions(-) diff --git a/.github/pr132-test.log b/.github/pr132-test.log index 614d5f4..36a90d0 100644 --- a/.github/pr132-test.log +++ b/.github/pr132-test.log @@ -1,126 +1,126 @@ Updating crates.io index Locking 94 packages to latest compatible versions Downloading crates ... - Downloaded anstyle v1.0.14 - Downloaded autocfg v1.5.1 Downloaded anes v0.1.6 - Downloaded serde_core v1.0.228 - Downloaded aho-corasick v1.1.4 - Downloaded cfg-if v1.0.4 - Downloaded crossbeam-epoch v0.9.20 - Downloaded find-msvc-tools v0.1.9 Downloaded ciborium-ll v0.2.2 - Downloaded clap v4.6.2 - Downloaded either v1.16.0 - Downloaded cc v1.2.67 - Downloaded ciborium v0.2.2 - Downloaded scopeguard v1.2.0 - Downloaded alloca v0.4.0 + Downloaded plotters-svg v0.3.7 Downloaded clap_lex v1.1.0 - Downloaded criterion-plot v0.8.2 - Downloaded serde_derive v1.0.228 - Downloaded itertools v0.13.0 - Downloaded crossbeam-deque v0.8.7 + Downloaded plotters-backend v0.3.7 Downloaded same-file v1.0.6 - Downloaded serde v1.0.228 - Downloaded memchr v2.8.3 - Downloaded zmij v1.0.23 Downloaded cast v0.3.0 + Downloaded scopeguard v1.2.0 + Downloaded criterion-plot v0.8.2 + Downloaded zmij v1.0.23 Downloaded thiserror v2.0.18 - Downloaded page_size v0.6.0 - Downloaded plotters-backend v0.3.7 Downloaded tinytemplate v1.2.1 - Downloaded walkdir v2.5.0 - Downloaded quote v1.0.46 - Downloaded oorandom v11.1.5 - Downloaded ciborium-io v0.2.2 - Downloaded itoa v1.0.18 - Downloaded parking_lot v0.12.5 + Downloaded smallvec v1.15.2 + Downloaded serde_derive v1.0.228 + Downloaded unicode-ident v1.0.24 + Downloaded serde_core v1.0.228 Downloaded proc-macro2 v1.0.106 - Downloaded crossbeam-utils v0.8.22 - Downloaded lock_api v0.4.14 + Downloaded serde v1.0.228 + Downloaded zerocopy-derive v0.8.54 + Downloaded memchr v2.8.3 + Downloaded criterion v0.8.2 Downloaded plotters v0.3.7 Downloaded serde_json v1.0.150 - Downloaded shlex v2.0.1 - Downloaded half v2.7.1 - Downloaded num-traits v0.2.19 - Downloaded rayon-core v1.13.0 - Downloaded clap_builder v4.6.2 - Downloaded plotters-svg v0.3.7 Downloaded regex v1.13.1 - Downloaded unicode-ident v1.0.24 - Downloaded criterion v0.8.2 - Downloaded thiserror-impl v2.0.18 - Downloaded libc v0.2.186 - Downloaded parking_lot_core v0.9.12 - Downloaded smallvec v1.15.2 - Downloaded zerocopy v0.8.54 - Downloaded zerocopy-derive v0.8.54 - Downloaded regex-syntax v0.8.11 Downloaded rayon v1.12.0 + Downloaded clap_builder v4.6.2 + Downloaded zerocopy v0.8.54 + Downloaded itertools v0.13.0 + Downloaded cc v1.2.67 + Downloaded rayon-core v1.13.0 Downloaded syn v2.0.119 + Downloaded half v2.7.1 + Downloaded crossbeam-epoch v0.9.20 + Downloaded regex-syntax v0.8.11 + Downloaded clap v4.6.2 + Downloaded num-traits v0.2.19 + Downloaded crossbeam-utils v0.8.22 + Downloaded ciborium v0.2.2 + Downloaded parking_lot_core v0.9.12 + Downloaded parking_lot v0.12.5 + Downloaded find-msvc-tools v0.1.9 + Downloaded crossbeam-deque v0.8.7 + Downloaded shlex v2.0.1 + Downloaded walkdir v2.5.0 + Downloaded thiserror-impl v2.0.18 + Downloaded quote v1.0.46 + Downloaded either v1.16.0 Downloaded regex-automata v0.4.16 + Downloaded autocfg v1.5.1 + Downloaded anstyle v1.0.14 + Downloaded oorandom v11.1.5 + Downloaded lock_api v0.4.14 + Downloaded aho-corasick v1.1.4 + Downloaded page_size v0.6.0 + Downloaded itoa v1.0.18 + Downloaded cfg-if v1.0.4 + Downloaded ciborium-io v0.2.2 + Downloaded alloca v0.4.0 + Downloaded libc v0.2.186 Compiling proc-macro2 v1.0.106 - Compiling unicode-ident v1.0.24 Compiling quote v1.0.46 + Compiling unicode-ident v1.0.24 Compiling find-msvc-tools v0.1.9 Compiling shlex v2.0.1 + Compiling libc v0.2.186 Compiling cc v1.2.67 Compiling serde_core v1.0.228 Compiling cfg-if v1.0.4 - Compiling libc v0.2.186 Compiling crossbeam-utils v0.8.22 Compiling zerocopy v0.8.54 Compiling crossbeam-epoch v0.9.20 - Compiling zmij v1.0.23 - Compiling crossbeam-deque v0.8.7 Compiling syn v2.0.119 - Compiling serde v1.0.228 Compiling autocfg v1.5.1 - Compiling parking_lot_core v0.9.12 + Compiling serde v1.0.228 + Compiling crossbeam-deque v0.8.7 + Compiling zmij v1.0.23 Compiling num-traits v0.2.19 Compiling serde_json v1.0.150 - Compiling rayon-core v1.13.0 Compiling either v1.16.0 + Compiling rayon-core v1.13.0 + Compiling parking_lot_core v0.9.12 Compiling alloca v0.4.0 - Compiling clap_lex v1.1.0 - Compiling anstyle v1.0.14 Compiling itoa v1.0.18 - Compiling plotters-backend v0.3.7 - Compiling thiserror v2.0.18 - Compiling smallvec v1.15.2 - Compiling regex-syntax v0.8.11 Compiling memchr v2.8.3 + Compiling anstyle v1.0.14 + Compiling plotters-backend v0.3.7 + Compiling clap_lex v1.1.0 Compiling scopeguard v1.2.0 + Compiling smallvec v1.15.2 Compiling ciborium-io v0.2.2 + Compiling regex-syntax v0.8.11 + Compiling thiserror v2.0.18 Compiling lock_api v0.4.14 - Compiling plotters-svg v0.3.7 Compiling clap_builder v4.6.2 - Compiling regex-automata v0.4.16 Compiling zerocopy-derive v0.8.54 Compiling serde_derive v1.0.228 Compiling thiserror-impl v2.0.18 + Compiling regex-automata v0.4.16 + Compiling plotters-svg v0.3.7 Compiling itertools v0.13.0 Compiling librtmp2 v0.4.0 (/home/runner/work/librtmp2/librtmp2) Compiling cast v0.3.0 Compiling same-file v1.0.6 - Compiling criterion-plot v0.8.2 Compiling walkdir v2.5.0 - Compiling half v2.7.1 + Compiling rayon v1.12.0 Compiling plotters v0.3.7 + Compiling half v2.7.1 Compiling ciborium-ll v0.2.2 - Compiling rayon v1.12.0 + Compiling criterion-plot v0.8.2 Compiling ciborium v0.2.2 Compiling tinytemplate v1.2.1 Compiling regex v1.13.1 Compiling clap v4.6.2 Compiling parking_lot v0.12.5 Compiling page_size v0.6.0 - Compiling anes v0.1.6 Compiling oorandom v11.1.5 + Compiling anes v0.1.6 Compiling criterion v0.8.2 - Finished `test` profile [unoptimized + debuginfo] target(s) in 27.35s - Running unittests src/lib.rs (target/debug/deps/librtmp2-ff643fd5d13d1ade) + Finished `test` profile [unoptimized + debuginfo] target(s) in 23.33s + Running unittests src/lib.rs (target/debug/deps/librtmp2-2900118f2168c46e) running 22 tests test client::tests::drain_ready_messages_splits_multitrack_video ... ok @@ -130,59 +130,101 @@ test ertmp::multitrack::tests::video_track_type_round_trips_as_amf0_double ... o test ertmp::multitrack_media::tests::coded_frames_x_keyframe_is_detected ... ok test ertmp::multitrack_media::tests::detects_multitrack_container ... ok test ertmp::multitrack_media::tests::iterates_many_tracks_many_codecs ... ok -test ertmp::multitrack_media::tests::iterates_subtracks_with_shared_codec ... ok test ertmp::multitrack_media::tests::malformed_message_delivers_no_partial_tracks ... ok -test media::init_cache::tests::multitrack_audio_callback_metadata_uses_track_descriptor ... ok +test ertmp::multitrack_media::tests::iterates_subtracks_with_shared_codec ... ok test media::init_cache::tests::multitrack_video_callback_metadata_uses_track_descriptor ... ok +test media::init_cache::tests::multitrack_audio_callback_metadata_uses_track_descriptor ... ok test media::init_cache::tests::multitrack_video_keyframe_is_cached_from_outer_header ... ok test media::init_cache::tests::multitrack_video_sequence_start_is_cached ... ok test server::tests::modex_wrapped_multitrack_is_detected_for_player_gating ... ok test server::tests::multitrack_per_track_video_inits_are_retained ... ok -test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... FAILED test server::tests::multitrack_video_sequence_header_is_cached ... ok -test session::conn::tests::multitrack_codec_is_detected_before_authorization ... ok test session::conn::tests::connect_with_caps_ex_enables_multitrack ... ok +test session::conn::tests::multitrack_codec_is_detected_before_authorization ... ok test session::conn::tests::multitrack_invokes_on_frame_cb_per_track ... ok test session::conn::tests::multitrack_on_frame_cb_scratch_retains_last_track_payload ... ok +test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... ok test ertmp::multitrack_media::tests::rejects_multitrack_messages_with_too_many_subtracks ... ok -failures: +test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 238 filtered out; finished in 0.00s + + Running tests/e2e_audio_frames.rs (target/debug/deps/e2e_audio_frames-c5cf8834dfd83223) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + Running tests/e2e_multiple_frames.rs (target/debug/deps/e2e_multiple_frames-1e68335c6abc44aa) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s ----- client::tests::drain_ready_messages_rejects_oversized_multitrack_video stdout ---- + Running tests/e2e_publish_play_same_listener.rs (target/debug/deps/e2e_publish_play_same_listener-c7be60aca89a5ede) -thread 'client::tests::drain_ready_messages_rejects_oversized_multitrack_video' (4178) panicked at src/client/mod.rs:1351:9: -assertion `left == right` failed - left: Ok(()) - right: Err(Protocol) -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +running 0 tests +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s -failures: - client::tests::drain_ready_messages_rejects_oversized_multitrack_video + Running tests/e2e_publish_rejection.rs (target/debug/deps/e2e_publish_rejection-c82b3e1c687ae677) -test result: FAILED. 21 passed; 1 failed; 0 ignored; 0 measured; 238 filtered out; finished in 0.00s +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + Running tests/ertmp_v2_review_regressions.rs (target/debug/deps/ertmp_v2_review_regressions-9b6fe8f982425938) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + + Running tests/server_client_loopback.rs (target/debug/deps/server_client_loopback-d4af3dbff581a592) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s -error: test failed, to rerun pass `--lib` Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s - Running unittests src/lib.rs (target/debug/deps/librtmp2-ff643fd5d13d1ade) + Running unittests src/lib.rs (target/debug/deps/librtmp2-2900118f2168c46e) running 1 test -test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... FAILED +test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 259 filtered out; finished in 0.00s + + Running tests/e2e_audio_frames.rs (target/debug/deps/e2e_audio_frames-c5cf8834dfd83223) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + Running tests/e2e_multiple_frames.rs (target/debug/deps/e2e_multiple_frames-1e68335c6abc44aa) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + Running tests/e2e_publish_play_same_listener.rs (target/debug/deps/e2e_publish_play_same_listener-c7be60aca89a5ede) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s + + Running tests/e2e_publish_rejection.rs (target/debug/deps/e2e_publish_rejection-c82b3e1c687ae677) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s -failures: + Running tests/ertmp_v2_review_regressions.rs (target/debug/deps/ertmp_v2_review_regressions-9b6fe8f982425938) ----- client::tests::drain_ready_messages_rejects_oversized_multitrack_video stdout ---- +running 0 tests -thread 'client::tests::drain_ready_messages_rejects_oversized_multitrack_video' (4208) panicked at src/client/mod.rs:1351:9: -assertion `left == right` failed - left: Ok(()) - right: Err(Protocol) -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + Running tests/server_client_loopback.rs (target/debug/deps/server_client_loopback-d4af3dbff581a592) -failures: - client::tests::drain_ready_messages_rejects_oversized_multitrack_video +running 0 tests -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 259 filtered out; finished in 0.00s +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s -error: test failed, to rerun pass `--lib` diff --git a/.github/pr132-test.status b/.github/pr132-test.status index 6432555..b748e2d 100644 --- a/.github/pr132-test.status +++ b/.github/pr132-test.status @@ -1 +1 @@ -101 101 +0 0 diff --git a/src/client/mod.rs b/src/client/mod.rs index baad796..d200ccc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1341,9 +1341,11 @@ mod tests { cmsg.msg_length = payload.len() as u32; cmsg.msg_type_id = msg_dispatch::RTMP_MSG_VIDEO; cmsg.msg_stream_id = 1; - chunk_write(&mut wire, &cmsg, &payload, payload.len(), 128).unwrap(); + let chunk_size = payload.len(); + chunk_write(&mut wire, &cmsg, &payload, payload.len(), chunk_size).unwrap(); let mut client = Client::new(); + client.chunk_reg.set_all_chunk_size(chunk_size as u32); client.recv_buffer.write(wire.peek()).unwrap(); client.on_frame_cb = Some(|_| panic!("invalid multitrack must not reach callback")); From 206b463b235dc47fc9fb8841005bb4fb7fa3394d Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 14:01:05 +0200 Subject: [PATCH 13/17] ci: remove PR 132 patch workflow --- .github/workflows/apply-pr132-review-fix.yml | 44 -------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/apply-pr132-review-fix.yml diff --git a/.github/workflows/apply-pr132-review-fix.yml b/.github/workflows/apply-pr132-review-fix.yml deleted file mode 100644 index 94e8bd5..0000000 --- a/.github/workflows/apply-pr132-review-fix.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Apply PR 132 review fix - -on: - pull_request: - branches: - - main - types: - - synchronize - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: cursor/application-security-review-6876 - fetch-depth: 0 - - - name: Apply review fix - run: python3 .github/scripts/pr132_review_fix.py - - - name: Format - run: cargo fmt --all - - - name: Capture test result - run: | - set +e - cargo test --no-default-features multitrack > .github/pr132-test.log 2>&1 - first_status=$? - cargo test --no-default-features drain_ready_messages_rejects_oversized_multitrack_video >> .github/pr132-test.log 2>&1 - second_status=$? - printf '%s %s\n' "$first_status" "$second_status" > .github/pr132-test.status - exit 0 - - - name: Commit diagnostics - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/session/conn.rs src/client/mod.rs .github/pr132-test.log .github/pr132-test.status - git commit -m "Apply multitrack rejection fix for validation" - git push origin HEAD:cursor/application-security-review-6876 From 48db43d20d4998100ca95e14955500e592ca9a1c Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 14:01:12 +0200 Subject: [PATCH 14/17] ci: remove PR 132 patch trigger --- .github/pr132-review-fix.trigger | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/pr132-review-fix.trigger diff --git a/.github/pr132-review-fix.trigger b/.github/pr132-review-fix.trigger deleted file mode 100644 index 854cb87..0000000 --- a/.github/pr132-review-fix.trigger +++ /dev/null @@ -1 +0,0 @@ -Apply the PR 132 review fix. From 883e50a7b17302f7e97234e8dc58997b04fdbce3 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 14:01:18 +0200 Subject: [PATCH 15/17] ci: remove PR 132 patch script --- .github/scripts/pr132_review_fix.py | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/scripts/pr132_review_fix.py diff --git a/.github/scripts/pr132_review_fix.py b/.github/scripts/pr132_review_fix.py deleted file mode 100644 index 8fff494..0000000 --- a/.github/scripts/pr132_review_fix.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import re -from pathlib import Path - -path = Path("src/client/mod.rs") -text = path.read_text() -pattern = re.compile( - r"( fn drain_ready_messages_rejects_oversized_multitrack_video\(\) \{" - r".*? cmsg\.msg_stream_id = 1;\n)" - r" chunk_write\(&mut wire, &cmsg, &payload, payload\.len\(\), 128\)\.unwrap\(\);\n\n" - r" let mut client = Client::new\(\);\n", - re.DOTALL, -) -replacement = ( - r"\1" - " let chunk_size = payload.len();\n" - " chunk_write(&mut wire, &cmsg, &payload, payload.len(), chunk_size).unwrap();\n\n" - " let mut client = Client::new();\n" - " client.chunk_reg.set_all_chunk_size(chunk_size as u32);\n" -) -updated, count = pattern.subn(replacement, text, count=1) -if count != 1: - raise RuntimeError(f"expected one oversized-multitrack test block, found {count}") -path.write_text(updated) From 8ad1c4344732d2cf50706cc3e07aa793ac9b0cf0 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 14:01:28 +0200 Subject: [PATCH 16/17] ci: remove PR 132 test log --- .github/pr132-test.log | 230 ----------------------------------------- 1 file changed, 230 deletions(-) delete mode 100644 .github/pr132-test.log diff --git a/.github/pr132-test.log b/.github/pr132-test.log deleted file mode 100644 index 36a90d0..0000000 --- a/.github/pr132-test.log +++ /dev/null @@ -1,230 +0,0 @@ - Updating crates.io index - Locking 94 packages to latest compatible versions - Downloading crates ... - Downloaded anes v0.1.6 - Downloaded ciborium-ll v0.2.2 - Downloaded plotters-svg v0.3.7 - Downloaded clap_lex v1.1.0 - Downloaded plotters-backend v0.3.7 - Downloaded same-file v1.0.6 - Downloaded cast v0.3.0 - Downloaded scopeguard v1.2.0 - Downloaded criterion-plot v0.8.2 - Downloaded zmij v1.0.23 - Downloaded thiserror v2.0.18 - Downloaded tinytemplate v1.2.1 - Downloaded smallvec v1.15.2 - Downloaded serde_derive v1.0.228 - Downloaded unicode-ident v1.0.24 - Downloaded serde_core v1.0.228 - Downloaded proc-macro2 v1.0.106 - Downloaded serde v1.0.228 - Downloaded zerocopy-derive v0.8.54 - Downloaded memchr v2.8.3 - Downloaded criterion v0.8.2 - Downloaded plotters v0.3.7 - Downloaded serde_json v1.0.150 - Downloaded regex v1.13.1 - Downloaded rayon v1.12.0 - Downloaded clap_builder v4.6.2 - Downloaded zerocopy v0.8.54 - Downloaded itertools v0.13.0 - Downloaded cc v1.2.67 - Downloaded rayon-core v1.13.0 - Downloaded syn v2.0.119 - Downloaded half v2.7.1 - Downloaded crossbeam-epoch v0.9.20 - Downloaded regex-syntax v0.8.11 - Downloaded clap v4.6.2 - Downloaded num-traits v0.2.19 - Downloaded crossbeam-utils v0.8.22 - Downloaded ciborium v0.2.2 - Downloaded parking_lot_core v0.9.12 - Downloaded parking_lot v0.12.5 - Downloaded find-msvc-tools v0.1.9 - Downloaded crossbeam-deque v0.8.7 - Downloaded shlex v2.0.1 - Downloaded walkdir v2.5.0 - Downloaded thiserror-impl v2.0.18 - Downloaded quote v1.0.46 - Downloaded either v1.16.0 - Downloaded regex-automata v0.4.16 - Downloaded autocfg v1.5.1 - Downloaded anstyle v1.0.14 - Downloaded oorandom v11.1.5 - Downloaded lock_api v0.4.14 - Downloaded aho-corasick v1.1.4 - Downloaded page_size v0.6.0 - Downloaded itoa v1.0.18 - Downloaded cfg-if v1.0.4 - Downloaded ciborium-io v0.2.2 - Downloaded alloca v0.4.0 - Downloaded libc v0.2.186 - Compiling proc-macro2 v1.0.106 - Compiling quote v1.0.46 - Compiling unicode-ident v1.0.24 - Compiling find-msvc-tools v0.1.9 - Compiling shlex v2.0.1 - Compiling libc v0.2.186 - Compiling cc v1.2.67 - Compiling serde_core v1.0.228 - Compiling cfg-if v1.0.4 - Compiling crossbeam-utils v0.8.22 - Compiling zerocopy v0.8.54 - Compiling crossbeam-epoch v0.9.20 - Compiling syn v2.0.119 - Compiling autocfg v1.5.1 - Compiling serde v1.0.228 - Compiling crossbeam-deque v0.8.7 - Compiling zmij v1.0.23 - Compiling num-traits v0.2.19 - Compiling serde_json v1.0.150 - Compiling either v1.16.0 - Compiling rayon-core v1.13.0 - Compiling parking_lot_core v0.9.12 - Compiling alloca v0.4.0 - Compiling itoa v1.0.18 - Compiling memchr v2.8.3 - Compiling anstyle v1.0.14 - Compiling plotters-backend v0.3.7 - Compiling clap_lex v1.1.0 - Compiling scopeguard v1.2.0 - Compiling smallvec v1.15.2 - Compiling ciborium-io v0.2.2 - Compiling regex-syntax v0.8.11 - Compiling thiserror v2.0.18 - Compiling lock_api v0.4.14 - Compiling clap_builder v4.6.2 - Compiling zerocopy-derive v0.8.54 - Compiling serde_derive v1.0.228 - Compiling thiserror-impl v2.0.18 - Compiling regex-automata v0.4.16 - Compiling plotters-svg v0.3.7 - Compiling itertools v0.13.0 - Compiling librtmp2 v0.4.0 (/home/runner/work/librtmp2/librtmp2) - Compiling cast v0.3.0 - Compiling same-file v1.0.6 - Compiling walkdir v2.5.0 - Compiling rayon v1.12.0 - Compiling plotters v0.3.7 - Compiling half v2.7.1 - Compiling ciborium-ll v0.2.2 - Compiling criterion-plot v0.8.2 - Compiling ciborium v0.2.2 - Compiling tinytemplate v1.2.1 - Compiling regex v1.13.1 - Compiling clap v4.6.2 - Compiling parking_lot v0.12.5 - Compiling page_size v0.6.0 - Compiling oorandom v11.1.5 - Compiling anes v0.1.6 - Compiling criterion v0.8.2 - Finished `test` profile [unoptimized + debuginfo] target(s) in 23.33s - Running unittests src/lib.rs (target/debug/deps/librtmp2-2900118f2168c46e) - -running 22 tests -test client::tests::drain_ready_messages_splits_multitrack_video ... ok -test ertmp::multitrack::tests::audio_track_type_round_trips ... ok -test ertmp::multitrack::tests::parse_error_leaves_descriptor_cleared ... ok -test ertmp::multitrack::tests::video_track_type_round_trips_as_amf0_double ... ok -test ertmp::multitrack_media::tests::coded_frames_x_keyframe_is_detected ... ok -test ertmp::multitrack_media::tests::detects_multitrack_container ... ok -test ertmp::multitrack_media::tests::iterates_many_tracks_many_codecs ... ok -test ertmp::multitrack_media::tests::malformed_message_delivers_no_partial_tracks ... ok -test ertmp::multitrack_media::tests::iterates_subtracks_with_shared_codec ... ok -test media::init_cache::tests::multitrack_video_callback_metadata_uses_track_descriptor ... ok -test media::init_cache::tests::multitrack_audio_callback_metadata_uses_track_descriptor ... ok -test media::init_cache::tests::multitrack_video_keyframe_is_cached_from_outer_header ... ok -test media::init_cache::tests::multitrack_video_sequence_start_is_cached ... ok -test server::tests::modex_wrapped_multitrack_is_detected_for_player_gating ... ok -test server::tests::multitrack_per_track_video_inits_are_retained ... ok -test server::tests::multitrack_video_sequence_header_is_cached ... ok -test session::conn::tests::connect_with_caps_ex_enables_multitrack ... ok -test session::conn::tests::multitrack_codec_is_detected_before_authorization ... ok -test session::conn::tests::multitrack_invokes_on_frame_cb_per_track ... ok -test session::conn::tests::multitrack_on_frame_cb_scratch_retains_last_track_payload ... ok -test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... ok -test ertmp::multitrack_media::tests::rejects_multitrack_messages_with_too_many_subtracks ... ok - -test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 238 filtered out; finished in 0.00s - - Running tests/e2e_audio_frames.rs (target/debug/deps/e2e_audio_frames-c5cf8834dfd83223) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_multiple_frames.rs (target/debug/deps/e2e_multiple_frames-1e68335c6abc44aa) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_publish_play_same_listener.rs (target/debug/deps/e2e_publish_play_same_listener-c7be60aca89a5ede) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_publish_rejection.rs (target/debug/deps/e2e_publish_rejection-c82b3e1c687ae677) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/ertmp_v2_review_regressions.rs (target/debug/deps/ertmp_v2_review_regressions-9b6fe8f982425938) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s - - Running tests/server_client_loopback.rs (target/debug/deps/server_client_loopback-d4af3dbff581a592) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s - - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s - Running unittests src/lib.rs (target/debug/deps/librtmp2-2900118f2168c46e) - -running 1 test -test client::tests::drain_ready_messages_rejects_oversized_multitrack_video ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 259 filtered out; finished in 0.00s - - Running tests/e2e_audio_frames.rs (target/debug/deps/e2e_audio_frames-c5cf8834dfd83223) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_multiple_frames.rs (target/debug/deps/e2e_multiple_frames-1e68335c6abc44aa) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_publish_play_same_listener.rs (target/debug/deps/e2e_publish_play_same_listener-c7be60aca89a5ede) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/e2e_publish_rejection.rs (target/debug/deps/e2e_publish_rejection-c82b3e1c687ae677) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - Running tests/ertmp_v2_review_regressions.rs (target/debug/deps/ertmp_v2_review_regressions-9b6fe8f982425938) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s - - Running tests/server_client_loopback.rs (target/debug/deps/server_client_loopback-d4af3dbff581a592) - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s - From 98165fbd77eb8756980bbb11b1783995d7051672 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Thu, 16 Jul 2026 14:01:38 +0200 Subject: [PATCH 17/17] ci: remove PR 132 test status --- .github/pr132-test.status | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/pr132-test.status diff --git a/.github/pr132-test.status b/.github/pr132-test.status deleted file mode 100644 index b748e2d..0000000 --- a/.github/pr132-test.status +++ /dev/null @@ -1 +0,0 @@ -0 0