[libc-commits] [libc] 289b604 - [libc][thread] detect self-join and mutual-join deadlock (#194891)

via libc-commits libc-commits at lists.llvm.org
Wed Apr 29 14:51:23 PDT 2026


Author: Schrodinger ZHU Yifan
Date: 2026-04-29T21:51:17Z
New Revision: 289b60430e10514dc1ba98ec2aea3226251f20ec

URL: https://github.com/llvm/llvm-project/commit/289b60430e10514dc1ba98ec2aea3226251f20ec
DIFF: https://github.com/llvm/llvm-project/commit/289b60430e10514dc1ba98ec2aea3226251f20ec.diff

LOG: [libc][thread] detect self-join and mutual-join deadlock (#194891)

Fix #194034.

Detect the deadlock cases of mutual thread joining.

Required by
`libcxx/test/std/thread/thread.jthread/join.deadlock.pass.cpp`


Assisted-by: Codex with gpt-5.5 high fast

Added: 
    

Modified: 
    libc/docs/dev/undefined_behavior.rst
    libc/src/__support/threads/linux/CMakeLists.txt
    libc/src/__support/threads/linux/thread.cpp
    libc/src/__support/threads/thread.h
    libc/test/integration/src/pthread/CMakeLists.txt
    libc/test/integration/src/pthread/pthread_join_test.cpp

Removed: 
    


################################################################################
diff  --git a/libc/docs/dev/undefined_behavior.rst b/libc/docs/dev/undefined_behavior.rst
index 59eafa4d86ffb..c777e5c371a65 100644
--- a/libc/docs/dev/undefined_behavior.rst
+++ b/libc/docs/dev/undefined_behavior.rst
@@ -180,3 +180,22 @@ For LLVM-libc, `tdelete` returns bit-casted `uintptr_t`'s maximum value.
 The standard requires that ``twalk``, ``twalk_r``, and ``tdestroy``
 to be used with a valid function pointer. LLVM-libc follows the behavior of
 configured via the `LIBC_ADD_NULL_CHECKS` option.
+
+Invalid Thread Joining Behavior
+------------------------------------------------------
+POSIX standard does not demand accurate deadlock detection and leaves
+repeated/concurrent joining as undefined behavior. In the following, 
+we discuss the related behaviors explicitly.
+
+Self joining is always rejected with ``EDEADLK``.
+
+At least one of the two threads in a mutual joining will detect the deadlock and
+return ``EDEADLK``. If joining requester gets ``EDEADLK``, the joining target is
+recovered to joinable state. However, the joining target may already be waiting
+if it does not see ``EDEADLK``.
+
+Cyclic joining with more than two threads is not detected.
+
+Concurrent and repeated joinings on the same thread are faulty behaviors, because
+target thread's TLS may already be torn down.  ``EINVAL`` may be returned if
+multiple joinings occur on the same thread but it is not guaranteed to observe.

diff  --git a/libc/src/__support/threads/linux/CMakeLists.txt b/libc/src/__support/threads/linux/CMakeLists.txt
index 5bd67c60ad88b..b9273b42bb5f1 100644
--- a/libc/src/__support/threads/linux/CMakeLists.txt
+++ b/libc/src/__support/threads/linux/CMakeLists.txt
@@ -33,6 +33,7 @@ add_object_library(
     libc.config.app_h
     libc.include.sys_syscall
     libc.hdr.fcntl_macros
+    libc.hdr.errno_macros
     libc.src.errno.errno
     libc.src.__support.CPP.atomic
     libc.src.__support.CPP.stringstream

diff  --git a/libc/src/__support/threads/linux/thread.cpp b/libc/src/__support/threads/linux/thread.cpp
index 4f1e9110457d9..bdbb8aefeef0c 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -22,6 +22,7 @@
 #include <arm_acle.h>
 #endif
 
+#include "hdr/errno_macros.h"
 #include "hdr/fcntl_macros.h"
 #include "hdr/stdint_proxy.h"
 #include <linux/param.h> // For EXEC_PAGESIZE.
@@ -282,6 +283,7 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
   attrib->owned_stack = owned_stack;
   attrib->tls = tls.addr;
   attrib->tls_size = tls.size;
+  attrib->joiner = nullptr;
 
   start_args->thread_attrib = attrib;
   start_args->runner = runner;
@@ -340,6 +342,26 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
 }
 
 int Thread::join(ThreadReturnValue &retval) {
+  if (self.attrib) {
+    // Reject self join.
+    if (self.attrib == attrib)
+      return EDEADLK;
+
+    // Do a best-effort check of concurrent/repeated join.
+    // This cmpxchg establishes exclusive joiner role by setting the joiner
+    // field iff there is no previous joiner
+    ThreadAttributes *expected = nullptr;
+    if (!attrib->joiner.compare_exchange_strong(expected, self.attrib,
+                                                cpp::MemoryOrder::ACQ_REL))
+      return EINVAL;
+
+    // Reject mutual join.
+    if (self.attrib->joiner.load(cpp::MemoryOrder::ACQUIRE) == attrib) {
+      attrib->joiner.store(nullptr, cpp::MemoryOrder::RELEASE);
+      return EDEADLK;
+    }
+  }
+
   wait();
 
   if (attrib->style == ThreadStyle::POSIX)

diff  --git a/libc/src/__support/threads/thread.h b/libc/src/__support/threads/thread.h
index 6806098653b2c..232b300bbba5b 100644
--- a/libc/src/__support/threads/thread.h
+++ b/libc/src/__support/threads/thread.h
@@ -109,12 +109,13 @@ struct alignas(STACK_ALIGNMENT) ThreadAttributes {
   ThreadReturnValue retval;
   ThreadAtExitCallbackMgr *atexit_callback_mgr;
   void *platform_data;
+  cpp::Atomic<ThreadAttributes *> joiner;
 
   LIBC_INLINE constexpr ThreadAttributes()
       : detach_state(uint32_t(DetachState::DETACHED)), stack(nullptr),
         stacksize(0), guardsize(0), tls(0), tls_size(0), owned_stack(false),
         tid(-1), style(ThreadStyle::POSIX), retval(),
-        atexit_callback_mgr(nullptr), platform_data(nullptr) {}
+        atexit_callback_mgr(nullptr), platform_data(nullptr), joiner(nullptr) {}
 };
 
 using TSSDtor = void(void *);

diff  --git a/libc/test/integration/src/pthread/CMakeLists.txt b/libc/test/integration/src/pthread/CMakeLists.txt
index eceebb42f18c4..32ff223ffa986 100644
--- a/libc/test/integration/src/pthread/CMakeLists.txt
+++ b/libc/test/integration/src/pthread/CMakeLists.txt
@@ -245,11 +245,14 @@ add_integration_test(
   SRCS
     pthread_join_test.cpp
   DEPENDS
+    libc.src.__support.CPP.atomic
+    libc.src.__support.threads.thread
     libc.include.pthread
     libc.include.stdio
     libc.src.errno.errno
     libc.src.pthread.pthread_create
     libc.src.pthread.pthread_join
+    libc.src.pthread.pthread_self
 )
 
 add_integration_test(

diff  --git a/libc/test/integration/src/pthread/pthread_join_test.cpp b/libc/test/integration/src/pthread/pthread_join_test.cpp
index 6dea99de1a64f..30b1cc93f4a5c 100644
--- a/libc/test/integration/src/pthread/pthread_join_test.cpp
+++ b/libc/test/integration/src/pthread/pthread_join_test.cpp
@@ -8,24 +8,101 @@
 
 #include "src/pthread/pthread_create.h"
 #include "src/pthread/pthread_join.h"
+#include "src/pthread/pthread_self.h"
 
+#include "src/__support/CPP/atomic.h"
+#include "src/__support/threads/thread.h"
 #include "test/IntegrationTest/test.h"
 
 #include <errno.h>
 #include <pthread.h>
 
-static void *simpleFunc(void *) { return nullptr; }
-static void nullJoinTest() {
-  pthread_t Tid;
-  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&Tid, nullptr, simpleFunc, nullptr),
+static void *simple_func(void *) { return nullptr; }
+
+static void null_join_test() {
+  pthread_t tid;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&tid, nullptr, simple_func, nullptr),
             0);
   ASSERT_ERRNO_SUCCESS();
-  ASSERT_EQ(LIBC_NAMESPACE::pthread_join(Tid, nullptr), 0);
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
   ASSERT_ERRNO_SUCCESS();
 }
 
+static void self_join_test() {
+  ASSERT_EQ(
+      LIBC_NAMESPACE::pthread_join(LIBC_NAMESPACE::pthread_self(), nullptr),
+      EDEADLK);
+}
+
+struct MutualJoinArgs {
+  pthread_t *peer;
+  LIBC_NAMESPACE::cpp::Atomic<int> *ready_count;
+  LIBC_NAMESPACE::cpp::Atomic<int> *start;
+  LIBC_NAMESPACE::cpp::Atomic<int> *result;
+  int start_value;
+};
+
+static void *mutual_join_func(void *arg) {
+  auto *args = reinterpret_cast<MutualJoinArgs *>(arg);
+  args->ready_count->fetch_add(1);
+  while (args->start->load() < args->start_value)
+    ; // Spin until this thread is released to join its peer.
+
+  args->result->store(LIBC_NAMESPACE::pthread_join(*args->peer, nullptr));
+  return nullptr;
+}
+
+static bool is_joining(pthread_t joiner, pthread_t target) {
+  auto *joiner_thread = reinterpret_cast<LIBC_NAMESPACE::Thread *>(&joiner);
+  auto *target_thread = reinterpret_cast<LIBC_NAMESPACE::Thread *>(&target);
+  return target_thread->attrib->joiner.load() == joiner_thread->attrib;
+}
+
+static void mutual_join_test() {
+  pthread_t thread1;
+  pthread_t thread2;
+  LIBC_NAMESPACE::cpp::Atomic<int> ready_count(0);
+  LIBC_NAMESPACE::cpp::Atomic<int> start(0);
+  LIBC_NAMESPACE::cpp::Atomic<int> result1(-1);
+  LIBC_NAMESPACE::cpp::Atomic<int> result2(-1);
+
+  MutualJoinArgs args1{&thread2, &ready_count, &start, &result1, 1};
+  MutualJoinArgs args2{&thread1, &ready_count, &start, &result2, 2};
+
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&thread1, nullptr, mutual_join_func,
+                                           &args1),
+            0);
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&thread2, nullptr, mutual_join_func,
+                                           &args2),
+            0);
+
+  while (ready_count.load() != 2)
+    ; // Spin until both threads are ready to join each other.
+  start.store(1);
+  while (!is_joining(thread1, thread2))
+    ; // Spin until thread1 has started joining thread2.
+  start.store(2);
+
+  while (result1.load() == -1 || result2.load() == -1)
+    ; // Spin until the successful joiner and deadlock loser have both exited.
+
+  // A thread is recovered to joinable state if its joining requester gets
+  // EDEADLK.
+  bool thread1_joinable = result2.load() == EDEADLK;
+  bool thread2_joinable = result1.load() == EDEADLK;
+
+  ASSERT_TRUE(thread1_joinable || thread2_joinable);
+
+  if (thread1_joinable)
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_join(thread1, nullptr), 0);
+  if (thread2_joinable)
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_join(thread2, nullptr), 0);
+}
+
 TEST_MAIN() {
   errno = 0;
-  nullJoinTest();
+  null_join_test();
+  self_join_test();
+  mutual_join_test();
   return 0;
 }


        


More information about the libc-commits mailing list