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 c79b3ffd Fix two races between butex_wait() and TaskGroup::interrupt() 
(#3551)
c79b3ffd is described below

commit c79b3ffd67b599deb8c0badd0dc75c854a28d0fd
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 22 10:38:21 2026 +0800

    Fix two races between butex_wait() and TaskGroup::interrupt() (#3551)
    
    * Fix two races between butex_wait() and TaskGroup::interrupt()
    
    * Fix self-deadlock in wait_for_butex() when TimerThread::schedule() fails
---
 src/bthread/butex.cpp       | 132 +++++++++++++++++++++++++++++++++-----------
 src/bthread/task_group.cpp  |   4 +-
 src/bthread/task_tracer.cpp |  40 ++++++++------
 src/bthread/task_tracer.h   |   1 +
 4 files changed, 125 insertions(+), 52 deletions(-)

diff --git a/src/bthread/butex.cpp b/src/bthread/butex.cpp
index 63920ca9..790da145 100644
--- a/src/bthread/butex.cpp
+++ b/src/bthread/butex.cpp
@@ -573,26 +573,62 @@ void wait_for_butex(void* arg) {
         BAIDU_SCOPED_LOCK(b->waiter_lock);
         if (b->value.load(butil::memory_order_relaxed) != bw->expected_value) {
             bw->waiter_state = WAITER_STATE_UNMATCHEDVALUE;
-        } else if (bw->waiter_state == WAITER_STATE_READY/*1*/ &&
-                   !bw->task_meta->interrupted) {
-            if (args->prepend) {
-                b->waiters.Prepend(bw);
-            } else {
-                b->waiters.Append(bw);
-            }
-            bw->container.store(b, butil::memory_order_relaxed);
+        } else {
+            // Checking `interrupted` and publishing `bw->container` must be
+            // atomic with respect to TaskGroup::interrupt(), which sets
+            // `interrupted` and consumes `current_waiter` under the same
+            // `version_lock`. Otherwise interrupt() may consume `bw` in 
between
+            // and its erase_from_butex() does nothing because `container` is
+            // still nullptr, leaving this bthread queued but never woken up.
+            // `container` cannot be published upfront: it must stay nullptr 
until
+            // the bthread is off its stack, see the comment after this block.
+            bool suspend = false;
+            {
+                // Only the interrupted check, the enqueue and the container
+                // store belong under `version_lock', so that they are atomic
+                // w.r.t. TaskGroup::interrupt(). The tracer update is kept 
here
+                // too because set_status_unsafe() takes no lock and this makes
+                // the transition to SUSPENDED atomic w.r.t. TraceImpl().
+                BAIDU_SCOPED_LOCK(bw->task_meta->version_lock);
+                if (bw->waiter_state == WAITER_STATE_READY/*1*/ &&
+                    !bw->task_meta->interrupted) {
+                    if (args->prepend) {
+                        b->waiters.Prepend(bw);
+                    } else {
+                        b->waiters.Append(bw);
+                    }
+                    bw->container.store(b, butil::memory_order_relaxed);
 #ifdef BRPC_BTHREAD_TRACER
-            bw->control->_task_tracer.set_status(TASK_STATUS_SUSPENDED, 
bw->task_meta);
+                    TaskTracer::set_status_unsafe(TASK_STATUS_SUSPENDED, 
bw->task_meta);
 #endif // BRPC_BTHREAD_TRACER
-            if (bw->abstime != nullptr) {
+                    suspend = true;
+                }
+            }
+            if (suspend && bw->abstime != nullptr) {
                 bw->sleep_id = get_global_timer_thread()->schedule(
                     erase_from_butex_and_wakeup, bw, *bw->abstime);
                 if (!bw->sleep_id) {  // TimerThread stopped.
+                    // No timer will ever fire, so `bw` must not stay queued.
+                    // CAUTION: erase_from_butex_and_wakeup() must NOT be 
called
+                    // here. It takes `waiter_lock` (already held) and then, 
via
+                    // TaskGroup::ready_to_run{,_remote}() ->
+                    // TaskTracer::set_status(), `version_lock` as well. Both 
are
+                    // non-recursive, so calling it self-deadlocks.
                     errno = ESTOP;
-                    erase_from_butex_and_wakeup(bw);
+                    bw->RemoveFromList();
+                    bw->container.store(nullptr, butil::memory_order_relaxed);
+                    bw->waiter_state = WAITER_STATE_TIMEDOUT;
+                    suspend = false;
                 }
             }
-            return;
+            if (suspend) {
+                return;
+            }
+            // Not suspended: the bthread has already been switched out by
+            // set_remained()/sched() in butex_wait(), so nobody else will run
+            // it. Fall through to re-schedule it below. This covers value
+            // unmatched, already timed out (waiter_state != READY), already
+            // interrupted, and the ESTOP path above.
         }
     }
     
@@ -619,33 +655,64 @@ static int butex_wait_from_pthread(TaskGroup* g, Butex* 
b, int expected_value,
     TaskMeta* task = nullptr;
     ButexPthreadWaiter pw;
     pw.tid = 0;
+    // Initialize `container` to nullptr before `pw` is published to
+    // `current_waiter` below: a concurrent TaskGroup::interrupt() may consume
+    // `pw` and call erase_from_butex() on it, which must observe either 
nullptr
+    // (a no-op) or a valid Butex, never stack garbage. `container` stays 
nullptr
+    // until `pw` is queued.
+    pw.container.store(nullptr, butil::memory_order_relaxed);
     pw.sig.store(PTHREAD_NOT_SIGNALLED, butil::memory_order_relaxed);
     int rc = 0;
-    
+
     if (g) {
         task = g->current_task();
         task->current_waiter.store(&pw, butil::memory_order_release);
     }
-    b->waiter_lock.lock();
-    if (b->value.load(butil::memory_order_relaxed) != expected_value) {
-        b->waiter_lock.unlock();
-        errno = EWOULDBLOCK;
-        rc = -1;
-    } else if (task != nullptr && task->interrupted) {
-        b->waiter_lock.unlock();
-        // Race with set and may consume multiple interruptions, which are OK.
-        task->interrupted = false;
-        errno = EINTR;
-        rc = -1;
-    } else {
-        if (prepend) {
-            b->waiters.Prepend(&pw);
+    bool queued = false;
+    {
+        BAIDU_SCOPED_LOCK(b->waiter_lock);
+        if (b->value.load(butil::memory_order_relaxed) != expected_value) {
+            errno = EWOULDBLOCK;
+            rc = -1;
+        } else if (task != nullptr) {
+            // Checking `interrupted` and publishing `container` must be atomic
+            // with respect to TaskGroup::interrupt(), which sets `interrupted`
+            // and consumes `current_waiter` under the same version_lock.
+            // Otherwise interrupt() may consume `pw' in between and its
+            // erase_from_butex() does nothing because `container' is still
+            // nullptr, leaving `pw` queued but never woken up.
+            BAIDU_SCOPED_LOCK(task->version_lock);
+            if (task->interrupted) {
+                // Already interrupted before queueing: consume it and return
+                // EINTR without blocking. An interruption that arrives after
+                // `pw` is queued is handled by the epilogue below instead.
+                // Race with set and may consume multiple interruptions,
+                // which are OK.
+                task->interrupted = false;
+                errno = EINTR;
+                rc = -1;
+            } else {
+                if (prepend) {
+                    b->waiters.Prepend(&pw);
+                } else {
+                    b->waiters.Append(&pw);
+                }
+                pw.container.store(b, butil::memory_order_relaxed);
+                queued = true;
+            }
         } else {
-            b->waiters.Append(&pw);
+            // A non-bthread pthread cannot be interrupted, so there is no race
+            // with interrupt() and no need to hold `version_lock` here.
+            if (prepend) {
+                b->waiters.Prepend(&pw);
+            } else {
+                b->waiters.Append(&pw);
+            }
+            pw.container.store(b, butil::memory_order_relaxed);
+            queued = true;
         }
-        pw.container.store(b, butil::memory_order_relaxed);
-        b->waiter_lock.unlock();
-
+    }
+    if (queued) {
 #ifdef SHOW_BTHREAD_BUTEX_WAITER_COUNT_IN_VARS
         bvar::Adder<int64_t>& num_waiters = butex_waiter_count();
         num_waiters << 1;
@@ -655,7 +722,8 @@ static int butex_wait_from_pthread(TaskGroup* g, Butex* b, 
int expected_value,
         num_waiters << -1;
 #endif
     }
-    if (task) {
+
+    if (task != nullptr) {
         // If current_waiter is nullptr, TaskGroup::interrupt() is running and
         // using pw, spin until current_waiter != nullptr.
         BT_LOOP_WHEN(task->current_waiter.exchange(
diff --git a/src/bthread/task_group.cpp b/src/bthread/task_group.cpp
index 0503fa4f..3051c07a 100644
--- a/src/bthread/task_group.cpp
+++ b/src/bthread/task_group.cpp
@@ -1048,7 +1048,7 @@ int TaskGroup::usleep(TaskGroup** pg, uint64_t 
timeout_us) {
 bool erase_from_butex_because_of_interruption(ButexWaiter* bw);
 
 static int interrupt_and_consume_waiters(
-    bthread_t tid, ButexWaiter** pw, uint64_t* sleep_id) {
+    bthread_t tid, ButexWaiter** bw, uint64_t* sleep_id) {
     TaskMeta* const m = TaskGroup::address_meta(tid);
     if (m == nullptr) {
         return EINVAL;
@@ -1056,7 +1056,7 @@ static int interrupt_and_consume_waiters(
     const uint32_t given_ver = get_version(tid);
     BAIDU_SCOPED_LOCK(m->version_lock);
     if (given_ver == *m->version_butex) {
-        *pw = m->current_waiter.exchange(nullptr, butil::memory_order_acquire);
+        *bw = m->current_waiter.exchange(nullptr, butil::memory_order_acquire);
         *sleep_id = m->current_sleep;
         m->current_sleep = 0;  // only one stopper gets the sleep_id
         m->interrupted = true;
diff --git a/src/bthread/task_tracer.cpp b/src/bthread/task_tracer.cpp
index 031ad793..afb1642d 100644
--- a/src/bthread/task_tracer.cpp
+++ b/src/bthread/task_tracer.cpp
@@ -161,27 +161,10 @@ bool TaskTracer::Init() {
 }
 
 void TaskTracer::set_status(TaskStatus s, TaskMeta* m) {
-    CHECK_NE(TASK_STATUS_RUNNING, s) << "Use `set_running_status' instead";
-    CHECK_NE(TASK_STATUS_END, s) << "Use `set_end_status_unsafe' instead";
-
     bool tracing = false;
     {
         BAIDU_SCOPED_LOCK(m->version_lock);
-        if (TASK_STATUS_UNKNOWN == m->status && TASK_STATUS_JUMPING == s) {
-            // Do not update status for jumping when bthread is ending.
-            return;
-        }
-
-        tracing = m->traced;
-        // bthread is scheduled for the first time.
-        if (TASK_STATUS_READY == s && nullptr == m->stack) {
-            m->status = TASK_STATUS_FIRST_READY;
-        } else {
-            m->status = s;
-        }
-        if (TASK_STATUS_CREATED == s) {
-            m->worker_tid = pthread_t{};
-        }
+        tracing = set_status_unsafe(s, m);
     }
 
     // Make sure bthread does not jump stack when it is being traced.
@@ -190,6 +173,27 @@ void TaskTracer::set_status(TaskStatus s, TaskMeta* m) {
     }
 }
 
+bool TaskTracer::set_status_unsafe(TaskStatus s, TaskMeta* m) {
+    CHECK_NE(TASK_STATUS_RUNNING, s) << "Use `set_running_status' instead";
+    CHECK_NE(TASK_STATUS_END, s) << "Use `set_end_status_unsafe' instead";
+
+    if (TASK_STATUS_UNKNOWN == m->status && TASK_STATUS_JUMPING == s) {
+        // Do not update status for jumping when bthread is ending.
+        return false;
+    }
+
+    // A bthread is scheduled for the first time.
+    if (TASK_STATUS_READY == s && m->stack == nullptr) {
+        m->status = TASK_STATUS_FIRST_READY;
+    } else {
+        m->status = s;
+    }
+    if (TASK_STATUS_CREATED == s) {
+        m->worker_tid = pthread_t{};
+    }
+    return m->traced;
+}
+
 void TaskTracer::set_running_status(pthread_t worker_tid, TaskMeta* m) {
     BAIDU_SCOPED_LOCK(m->version_lock);
     m->worker_tid = worker_tid;
diff --git a/src/bthread/task_tracer.h b/src/bthread/task_tracer.h
index 8844413a..2bf4099b 100644
--- a/src/bthread/task_tracer.h
+++ b/src/bthread/task_tracer.h
@@ -39,6 +39,7 @@ public:
     bool Init();
     // Set the status to `s'.
     void set_status(TaskStatus s, TaskMeta* meta);
+    static bool set_status_unsafe(TaskStatus s, TaskMeta* meta);
     static void set_running_status(pthread_t worker_tid, TaskMeta* meta);
     static bool set_end_status_unsafe(TaskMeta* m);
 


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

Reply via email to