This is an automated email from the ASF dual-hosted git repository.

wasphin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new e53520f4 Preserve RTMP chunk stream state on abort (#3515)
e53520f4 is described below

commit e53520f48c7b13d3bb9d0f34fa2fb80e13431043
Author: Xiaofeng Wang <[email protected]>
AuthorDate: Thu Sep 3 13:30:42 2026 +0800

    Preserve RTMP chunk stream state on abort (#3515)
    
    Reset the incomplete message when processing an RTMP Abort command instead
    of deleting the chunk stream. This keeps the previous message header needed
    by subsequent compressed chunk headers.
---
 src/brpc/policy/rtmp_protocol.cpp | 18 +++++------
 src/brpc/policy/rtmp_protocol.h   |  6 ++--
 test/brpc_rtmp_unittest.cpp       | 63 +++++++++++++++++++++++++++++++++++----
 3 files changed, 71 insertions(+), 16 deletions(-)

diff --git a/src/brpc/policy/rtmp_protocol.cpp 
b/src/brpc/policy/rtmp_protocol.cpp
index d72684b1..fbc0ed0e 100644
--- a/src/brpc/policy/rtmp_protocol.cpp
+++ b/src/brpc/policy/rtmp_protocol.cpp
@@ -860,7 +860,7 @@ RtmpChunkStream* RtmpContext::GetChunkStream(uint32_t 
cs_id) {
     return cstream;
 }
 
-void RtmpContext::ClearChunkStream(uint32_t cs_id) {
+void RtmpContext::AbortChunkStream(uint32_t cs_id) {
     if (cs_id > RTMP_MAX_CHUNK_STREAM_ID) {
         LOG(ERROR) << "Invalid chunk_stream_id=" << cs_id;
         return;
@@ -879,8 +879,7 @@ void RtmpContext::ClearChunkStream(uint32_t cs_id) {
         LOG(ERROR) << "chunk_stream_id=" << cs_id << " does not exist";
         return;
     }
-    delete sub_array->ptrs[index2].exchange(
-        nullptr, butil::memory_order_acquire);
+    cstream->OnAbort();
 }
 
 void RtmpContext::AllocateChunkStreamId(uint32_t* chunk_stream_id) {
@@ -1773,6 +1772,12 @@ int RtmpChunkStream::SerializeMessage(butil::IOBuf* buf,
     return 0;
 }
 
+void RtmpChunkStream::OnAbort() {
+    _r.msg_body.clear();
+    _r.left_message_length = _r.last_msg_header.message_length;
+    _r.first_chunk_of_message = true;
+}
+
 static const RtmpChunkStream::MessageHandler s_msg_handlers[] = {
     &RtmpChunkStream::OnSetChunkSize, // 1
     &RtmpChunkStream::OnAbortMessage, // 2
@@ -1903,12 +1908,7 @@ bool RtmpChunkStream::OnAbortMessage(
         RTMP_ERROR(socket, mh) << "Invalid chunk_stream_id=" << cs_id;
         return false;
     }
-    // Do not delete the chunk stream that is currently being parsed (i.e.
-    // the one running this Feed). Clearing it here would free `this' while
-    // Feed() still holds and later touches it, causing a use-after-free.
-    if (cs_id != _cs_id) {
-        connection_context()->ClearChunkStream(cs_id);
-    }
+    connection_context()->AbortChunkStream(cs_id);
     return true;
 }
 
diff --git a/src/brpc/policy/rtmp_protocol.h b/src/brpc/policy/rtmp_protocol.h
index 2ed9fd3c..c5e14ca6 100644
--- a/src/brpc/policy/rtmp_protocol.h
+++ b/src/brpc/policy/rtmp_protocol.h
@@ -306,8 +306,8 @@ public:
 
     // Get the chunk stream by its id. The stream is created by need.
     RtmpChunkStream* GetChunkStream(uint32_t cs_id);
-    // Reset the chunk stream associated with the id.
-    void ClearChunkStream(uint32_t cs_id);
+    // Discard the incomplete message on the chunk stream, if it exists.
+    void AbortChunkStream(uint32_t cs_id);
 
     // Allocate/deallocate id for a chunk stream.
     void AllocateChunkStreamId(uint32_t* chunk_stream_id);
@@ -424,6 +424,8 @@ public:
 
     int SerializeMessage(butil::IOBuf* buf, const RtmpMessageHeader& mh,
                          butil::IOBuf* body);
+
+    void OnAbort();
     
     bool OnMessage(
         const RtmpBasicHeader& bh, const RtmpMessageHeader& mh,
diff --git a/test/brpc_rtmp_unittest.cpp b/test/brpc_rtmp_unittest.cpp
index eb39f417..4315031e 100644
--- a/test/brpc_rtmp_unittest.cpp
+++ b/test/brpc_rtmp_unittest.cpp
@@ -918,11 +918,9 @@ TEST(RtmpTest, flv_reader_rejects_zero_datasize_audio_tag) 
{
     ASSERT_EQ(before, buf.size());
 }
 
-// A crafted Abort message that names the chunk stream currently being parsed
-// (itself) used to make ClearChunkStream delete the RtmpChunkStream while its
-// Feed() is still running, which caused a heap-use-after-free right after
-// OnMessage() returned.
-TEST(RtmpTest, abort_message_naming_own_chunk_stream) {
+// Abort discards an incomplete message without deleting the chunk stream,
+// whether it names the stream carrying the Abort or a different stream.
+TEST(RtmpTest, abort_message_preserves_chunk_stream_state) {
     int pipe_fds[2];
     ASSERT_EQ(0, pipe(pipe_fds));
     butil::fd_guard guard0(pipe_fds[0]);   // read end, closed by this guard
@@ -973,6 +971,61 @@ TEST(RtmpTest, abort_message_naming_own_chunk_stream) {
     butil::IOBuf buf2;
     buf2.append(cont);
     ASSERT_EQ(brpc::PARSE_OK, ctx.Feed(&buf2, sock.get()).error());
+
+    // Reduce the inbound chunk size to split four-byte control messages.
+    std::string set_chunk_size;
+    set_chunk_size.push_back((char)0x02);  // fmt=0, cs_id=2
+    set_chunk_size.append(3, '\0');        // timestamp = 0
+    AppendBigEndian3Bytes(&set_chunk_size, 4);
+    set_chunk_size.push_back((char)0x01);  // message_type = SetChunkSize
+    set_chunk_size.append(4, '\0');        // stream_id = 0
+    set_chunk_size.append(3, '\0');
+    set_chunk_size.push_back((char)0x02);  // new chunk size = 2
+    butil::IOBuf set_chunk_size_buf;
+    set_chunk_size_buf.append(set_chunk_size);
+    ASSERT_EQ(brpc::PARSE_OK,
+              ctx.Feed(&set_chunk_size_buf, sock.get()).error());
+
+    // Start an Ack on chunk stream 3, leaving half of its body incomplete.
+    std::string partial_ack;
+    partial_ack.push_back((char)0x03);     // fmt=0, cs_id=3
+    partial_ack.append(3, '\0');           // timestamp = 0
+    AppendBigEndian3Bytes(&partial_ack, 4);
+    partial_ack.push_back((char)0x03);     // message_type = Ack
+    partial_ack.append(4, '\0');           // stream_id = 0
+    partial_ack.append(2, '\0');           // first two body bytes
+    butil::IOBuf partial_ack_buf;
+    partial_ack_buf.append(partial_ack);
+    ASSERT_EQ(brpc::PARSE_OK,
+              ctx.Feed(&partial_ack_buf, sock.get()).error());
+
+    // Abort chunk stream 3. The Abort itself is split over two chunks because
+    // the inbound chunk size is now two bytes.
+    std::string abort_first;
+    abort_first.push_back((char)0x02);     // fmt=0, cs_id=2
+    abort_first.append(3, '\0');           // timestamp = 0
+    AppendBigEndian3Bytes(&abort_first, 4);
+    abort_first.push_back((char)0x02);     // message_type = Abort
+    abort_first.append(4, '\0');           // stream_id = 0
+    abort_first.append(2, '\0');           // first two bytes of cs_id
+    butil::IOBuf abort_first_buf;
+    abort_first_buf.append(abort_first);
+    ASSERT_EQ(brpc::PARSE_OK,
+              ctx.Feed(&abort_first_buf, sock.get()).error());
+
+    const char abort_last[] = { (char)0xC2, 0, 3 };  // fmt=3, cs_id=2
+    butil::IOBuf abort_last_buf;
+    abort_last_buf.append(abort_last, sizeof(abort_last));
+    ASSERT_EQ(brpc::PARSE_OK,
+              ctx.Feed(&abort_last_buf, sock.get()).error());
+
+    // A type-3 chunk starts a new message using stream 3's previous header.
+    // Deleting the stream on Abort would lose that header and reject this.
+    const char ack_part[] = { (char)0xC3, 0, 0 };  // fmt=3, cs_id=3
+    butil::IOBuf ack_part1_buf;
+    ack_part1_buf.append(ack_part, sizeof(ack_part));
+    ASSERT_EQ(brpc::PARSE_OK,
+              ctx.Feed(&ack_part1_buf, sock.get()).error());
 }
 
 TEST(RtmpTest, rejected_create_stream_with_response_write_failure) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to