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

chenBright 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 5866e11d Fix use-after-free in RTMP test sender lifetime management 
(#3543)
5866e11d is described below

commit 5866e11d624ed375eb2925bcce276dcdfe91baba
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 22 14:02:27 2026 +0800

    Fix use-after-free in RTMP test sender lifetime management (#3543)
    
    * Fix use-after-free in RTMP test sender lifetime management
    
    * Fix skipped bthread_stop
    
    * Make the ownership clear
---
 test/brpc_rtmp_unittest.cpp | 107 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 105 insertions(+), 2 deletions(-)

diff --git a/test/brpc_rtmp_unittest.cpp b/test/brpc_rtmp_unittest.cpp
index 4315031e..397e5d3b 100644
--- a/test/brpc_rtmp_unittest.cpp
+++ b/test/brpc_rtmp_unittest.cpp
@@ -269,9 +269,16 @@ public:
                       << " ms before responding play request";
             bthread_usleep(_sleep_ms * 1000L);
         }
+        // Reserve a reference for the sender bthread, keeping the stream alive
+        // until SendData() returns, even if a failed send synchronously runs
+        // OnStop() and releases the framework's references. The reference
+        // belongs to the sender from the moment it is acquired, so only the
+        // failed creation path gives it back.
+        AddRefManually();
         int rc = bthread_start_background(&_play_thread, nullptr,
                                           RunSendData, this);
         if (rc) {
+            RemoveRefManually();
             status->set_error(rc, "Fail to create thread");
             return;
         }
@@ -289,8 +296,17 @@ public:
     void OnStop() {
         LOG(INFO) << "OnStop of PlayingDummyStream=" << this;
         if (_state.exchange(STATE_STOPPED) == STATE_PLAYING) {
+            // A send failure can invoke this callback on the sender bthread
+            // itself. bthread_stop() only sets the stop flag that SendData's
+            // loop checks, so it must be called on the current bthread too,
+            // otherwise the sender loops and sleeps forever while holding its
+            // stream reference. Only joining the current bthread is unsafe, so
+            // skip just the join on the self-callback path; the sender's own
+            // reference keeps the stream alive until SendData returns.
             bthread_stop(_play_thread);
-            bthread_join(_play_thread, nullptr);
+            if (_play_thread != bthread_self()) {
+                bthread_join(_play_thread, nullptr);
+            }
         }
     }
 
@@ -298,7 +314,10 @@ public:
     
 private:
     static void* RunSendData(void* arg) {
-        ((PlayingDummyStream*)arg)->SendData();
+        // Adopt the reference acquired before starting this bthread.
+        butil::intrusive_ptr<PlayingDummyStream> stream(
+            static_cast<PlayingDummyStream*>(arg), false);
+        stream->SendData();
         return nullptr;
     }
 
@@ -342,6 +361,90 @@ void PlayingDummyStream::SendData() {
     LOG(INFO) << "Quit SendData of PlayingDummyStream=" << this;
 }
 
+// Regression test for a use-after-free where a send failure synchronously ran
+// OnStop() on the sender bthread and dropped the framework's reference while
+// SendData() was still using the stream. This reproduces the ordering
+// deterministically without a socket: the sender bthread runs OnStop() itself,
+// which releases the framework's reference, and then keeps touching the 
stream.
+class SelfStopStream : public brpc::RtmpServerStream {
+public:
+    SelfStopStream(butil::atomic<bool>* destroyed,
+                   butil::atomic<bool>* alive_after_stop,
+                   butil::atomic<bool>* stop_on_sender)
+        : _destroyed(destroyed)
+        , _alive_after_stop(alive_after_stop)
+        , _stop_on_sender(stop_on_sender)
+        , _sender_ready(false) {}
+    ~SelfStopStream() override {
+        _destroyed->store(true, butil::memory_order_relaxed);
+    }
+
+    void OnStop() override {
+        _stop_on_sender->store(_sender == bthread_self(),
+                               butil::memory_order_relaxed);
+        // Drop the framework's reference from within OnStop(), exactly like
+        // RtmpServerStream::RunOnFailed does on the sender bthread.
+        _framework_ref.reset();
+    }
+
+    // Starts the sender bthread and returns its id. The framework keeps one
+    // reference until OnStop() drops it; a second reference is reserved for
+    // the sender bthread, exactly like PlayingDummyStream::OnPlay does.
+    bthread_t Start() {
+        _framework_ref.reset(this);
+        AddRefManually();
+        CHECK_EQ(0, bthread_start_background(
+                        &_sender, nullptr, RunSender, this));
+        // Publish _sender before the sender bthread reads it in OnStop().
+        _sender_ready.store(true, butil::memory_order_release);
+        return _sender;
+    }
+
+private:
+    static void* RunSender(void* arg) {
+        butil::intrusive_ptr<SelfStopStream> self(
+            static_cast<SelfStopStream*>(arg), false);
+        while (!self->_sender_ready.load(butil::memory_order_acquire)) {
+            bthread_usleep(100);
+        }
+        // Run OnStop() on this very bthread, releasing the framework 
reference.
+        self->OnStop();
+        // The stream must still be alive here, kept by the sender's reference.
+        self->_alive_after_stop->store(
+            !self->_destroyed->load(butil::memory_order_relaxed) &&
+                self->ref_count() >= 1,
+            butil::memory_order_relaxed);
+        return nullptr;
+    }
+
+    butil::atomic<bool>* _destroyed;
+    butil::atomic<bool>* _alive_after_stop;
+    butil::atomic<bool>* _stop_on_sender;
+    butil::atomic<bool> _sender_ready;
+    bthread_t _sender;
+    butil::intrusive_ptr<SelfStopStream> _framework_ref;
+};
+
+TEST(RtmpTest, on_stop_on_sender_bthread_keeps_stream_alive) {
+    butil::atomic<bool> destroyed(false);
+    butil::atomic<bool> alive_after_stop(false);
+    butil::atomic<bool> stop_on_sender(false);
+    bthread_t sender;
+    {
+        butil::intrusive_ptr<SelfStopStream> stream(
+            new SelfStopStream(&destroyed, &alive_after_stop, 
&stop_on_sender));
+        sender = stream->Start();
+        ASSERT_EQ(0, bthread_join(sender, nullptr));
+        // OnStop ran on the sender bthread and the stream survived it.
+        ASSERT_TRUE(stop_on_sender.load(butil::memory_order_relaxed));
+        ASSERT_TRUE(alive_after_stop.load(butil::memory_order_relaxed));
+        // The sender's reference is gone but the test still holds one.
+        ASSERT_FALSE(destroyed.load(butil::memory_order_relaxed));
+    }
+    // The last reference is gone now, the stream must be destroyed.
+    ASSERT_TRUE(destroyed.load(butil::memory_order_relaxed));
+}
+
 class PlayingDummyService : public brpc::RtmpService {
 public:
     PlayingDummyService(int64_t sleep_ms = 0) : _sleep_ms(sleep_ms) {}


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

Reply via email to