Skip to content

Conversation

ghostway0
Copy link

@ghostway0 ghostway0 commented Sep 4, 2025

fixes #146683 #156829

When a process calls pthread_{join,detach} twice, TSAN crashes when it doesn't find the current thread to remove from the live threads map.

This patch makes it output something like:
==29712==ThreadSanitizer: pthread_detach was called on thread 0 but it is dead.

Copy link

github-actions bot commented Sep 4, 2025

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Sep 4, 2025

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: None (ghostway0)

Changes

fixes #146683 #156829

outputs something like:
==29712==ThreadSanitizer: pthread_detach was called on thread 0 but it is dead.
detach rc1=0 rc2=-22


Full diff: https://github.com/llvm/llvm-project/pull/156921.diff

5 Files Affected:

  • (modified) compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp (+12-5)
  • (modified) compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h (+1-1)
  • (modified) compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp (+20-4)
  • (modified) compiler-rt/lib/tsan/rtl/tsan_rtl.h (+1-1)
  • (modified) compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp (+2-2)
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
index d726d282437ca..b8f32b52c64d8 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
@@ -274,6 +274,11 @@ void ThreadRegistry::JoinThread(u32 tid, void *arg) {
     {
       ThreadRegistryLock l(this);
       ThreadContextBase *tctx = threads_[tid];
+
+      if (!tctx) {
+        Report("%s: Tried to join thread %u, but it is not live.", SanitizerToolName, tid);
+      }
+
       CHECK_NE(tctx, 0);
       if (tctx->status == ThreadStatusInvalid) {
         Report("%s: Join of non-existent thread\n", SanitizerToolName);
@@ -357,17 +362,19 @@ ThreadContextBase *ThreadRegistry::QuarantinePop() {
   return tctx;
 }
 
-u32 ThreadRegistry::ConsumeThreadUserId(uptr user_id) {
+bool ThreadRegistry::ConsumeThreadUserId(uptr user_id, u32 *tid_out) {
   ThreadRegistryLock l(this);
-  u32 tid;
   auto *t = live_.find(user_id);
-  CHECK(t);
-  tid = t->second;
+  if (!t) {
+    return false;
+  }
+  u32 tid = t->second;
   live_.erase(t);
   auto *tctx = threads_[tid];
   CHECK_EQ(tctx->user_id, user_id);
   tctx->user_id = 0;
-  return tid;
+  *tid_out = tid;
+  return true;
 }
 
 void ThreadRegistry::SetThreadUserId(u32 tid, uptr user_id) {
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
index 8adc420c8cce4..83459685fcb53 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
@@ -135,7 +135,7 @@ class SANITIZER_MUTEX ThreadRegistry {
   // Finishes thread and returns previous status.
   ThreadStatus FinishThread(u32 tid);
   void StartThread(u32 tid, ThreadID os_id, ThreadType thread_type, void *arg);
-  u32 ConsumeThreadUserId(uptr user_id);
+  bool ConsumeThreadUserId(uptr user_id, u32 *tid_out);
   void SetThreadUserId(u32 tid, uptr user_id);
 
   // OnFork must be called in the child process after fork to purge old
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
index b46a81031258c..4eabea5addd2a 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
@@ -1130,7 +1130,11 @@ TSAN_INTERCEPTOR(int, pthread_create,
 
 TSAN_INTERCEPTOR(int, pthread_join, void *th, void **ret) {
   SCOPED_INTERCEPTOR_RAW(pthread_join, th, ret);
-  Tid tid = ThreadConsumeTid(thr, pc, (uptr)th);
+  Tid tid;
+  if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
+    Report("ThreadSanitizer: pthread_join was called on thread %d but it is dead.\n", thr->tid);
+    return -errno_EINVAL;
+  }
   ThreadIgnoreBegin(thr, pc);
   int res = BLOCK_REAL(pthread_join)(th, ret);
   ThreadIgnoreEnd(thr);
@@ -1155,7 +1159,11 @@ int internal_pthread_join(void *th, void **ret) {
 
 TSAN_INTERCEPTOR(int, pthread_detach, void *th) {
   SCOPED_INTERCEPTOR_RAW(pthread_detach, th);
-  Tid tid = ThreadConsumeTid(thr, pc, (uptr)th);
+  Tid tid;
+  if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
+    Report("ThreadSanitizer: pthread_detach was called on thread %d but it is dead.\n", thr->tid);
+    return -errno_EINVAL;
+  }
   int res = REAL(pthread_detach)(th);
   if (res == 0) {
     ThreadDetach(thr, pc, tid);
@@ -1176,7 +1184,11 @@ TSAN_INTERCEPTOR(void, pthread_exit, void *retval) {
 #if SANITIZER_LINUX
 TSAN_INTERCEPTOR(int, pthread_tryjoin_np, void *th, void **ret) {
   SCOPED_INTERCEPTOR_RAW(pthread_tryjoin_np, th, ret);
-  Tid tid = ThreadConsumeTid(thr, pc, (uptr)th);
+  Tid tid;
+  if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
+    Report("ThreadSanitizer: pthread_tryjoin_np was called on thread %d but it is dead.\n", thr->tid);
+    return -errno_EINVAL;
+  }
   ThreadIgnoreBegin(thr, pc);
   int res = REAL(pthread_tryjoin_np)(th, ret);
   ThreadIgnoreEnd(thr);
@@ -1190,7 +1202,11 @@ TSAN_INTERCEPTOR(int, pthread_tryjoin_np, void *th, void **ret) {
 TSAN_INTERCEPTOR(int, pthread_timedjoin_np, void *th, void **ret,
                  const struct timespec *abstime) {
   SCOPED_INTERCEPTOR_RAW(pthread_timedjoin_np, th, ret, abstime);
-  Tid tid = ThreadConsumeTid(thr, pc, (uptr)th);
+  Tid tid;
+  if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
+    Report("ThreadSanitizer: pthread_timedjoin_np was called on thread %d but it is dead.\n", thr->tid);
+    return -errno_EINVAL;
+  }
   ThreadIgnoreBegin(thr, pc);
   int res = BLOCK_REAL(pthread_timedjoin_np)(th, ret, abstime);
   ThreadIgnoreEnd(thr);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl.h b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
index 0b6d5f088b142..03bd4d8de2e28 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
@@ -563,7 +563,7 @@ Tid ThreadCreate(ThreadState *thr, uptr pc, uptr uid, bool detached);
 void ThreadStart(ThreadState *thr, Tid tid, ThreadID os_id,
                  ThreadType thread_type);
 void ThreadFinish(ThreadState *thr);
-Tid ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid);
+bool ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid, Tid *tid_out);
 void ThreadJoin(ThreadState *thr, uptr pc, Tid tid);
 void ThreadDetach(ThreadState *thr, uptr pc, Tid tid);
 void ThreadFinalize(ThreadState *thr);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp b/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
index b1464ccfddeb4..fd33dde7e48db 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
@@ -301,8 +301,8 @@ struct ConsumeThreadContext {
   ThreadContextBase *tctx;
 };
 
-Tid ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid) {
-  return ctx->thread_registry.ConsumeThreadUserId(uid);
+bool ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid, Tid *tid_out) {
+  return ctx->thread_registry.ConsumeThreadUserId(uid, tid_out);
 }
 
 struct JoinArg {

Copy link

github-actions bot commented Sep 4, 2025

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions cpp,h -- compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp compiler-rt/lib/tsan/rtl/tsan_rtl.h compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
index b8f32b52c..aaac2e852 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.cpp
@@ -276,7 +276,8 @@ void ThreadRegistry::JoinThread(u32 tid, void *arg) {
       ThreadContextBase *tctx = threads_[tid];
 
       if (!tctx) {
-        Report("%s: Tried to join thread %u, but it is not live.", SanitizerToolName, tid);
+        Report("%s: Tried to join thread %u, but it is not live.",
+               SanitizerToolName, tid);
       }
 
       CHECK_NE(tctx, 0);
@@ -362,7 +363,7 @@ ThreadContextBase *ThreadRegistry::QuarantinePop() {
   return tctx;
 }
 
-bool ThreadRegistry::ConsumeThreadUserId(uptr user_id, u32 *tid_out) {
+bool ThreadRegistry::ConsumeThreadUserId(uptr user_id, u32* tid_out) {
   ThreadRegistryLock l(this);
   auto *t = live_.find(user_id);
   if (!t) {
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
index 83459685f..e4398f323 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_thread_registry.h
@@ -135,7 +135,7 @@ class SANITIZER_MUTEX ThreadRegistry {
   // Finishes thread and returns previous status.
   ThreadStatus FinishThread(u32 tid);
   void StartThread(u32 tid, ThreadID os_id, ThreadType thread_type, void *arg);
-  bool ConsumeThreadUserId(uptr user_id, u32 *tid_out);
+  bool ConsumeThreadUserId(uptr user_id, u32* tid_out);
   void SetThreadUserId(u32 tid, uptr user_id);
 
   // OnFork must be called in the child process after fork to purge old
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
index 4eabea5ad..aa5f4e2d1 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
@@ -1132,7 +1132,10 @@ TSAN_INTERCEPTOR(int, pthread_join, void *th, void **ret) {
   SCOPED_INTERCEPTOR_RAW(pthread_join, th, ret);
   Tid tid;
   if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
-    Report("ThreadSanitizer: pthread_join was called on thread %d but it is dead.\n", thr->tid);
+    Report(
+        "ThreadSanitizer: pthread_join was called on thread %d but it is "
+        "dead.\n",
+        thr->tid);
     return -errno_EINVAL;
   }
   ThreadIgnoreBegin(thr, pc);
@@ -1161,7 +1164,10 @@ TSAN_INTERCEPTOR(int, pthread_detach, void *th) {
   SCOPED_INTERCEPTOR_RAW(pthread_detach, th);
   Tid tid;
   if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
-    Report("ThreadSanitizer: pthread_detach was called on thread %d but it is dead.\n", thr->tid);
+    Report(
+        "ThreadSanitizer: pthread_detach was called on thread %d but it is "
+        "dead.\n",
+        thr->tid);
     return -errno_EINVAL;
   }
   int res = REAL(pthread_detach)(th);
@@ -1186,7 +1192,10 @@ TSAN_INTERCEPTOR(int, pthread_tryjoin_np, void *th, void **ret) {
   SCOPED_INTERCEPTOR_RAW(pthread_tryjoin_np, th, ret);
   Tid tid;
   if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
-    Report("ThreadSanitizer: pthread_tryjoin_np was called on thread %d but it is dead.\n", thr->tid);
+    Report(
+        "ThreadSanitizer: pthread_tryjoin_np was called on thread %d but it is "
+        "dead.\n",
+        thr->tid);
     return -errno_EINVAL;
   }
   ThreadIgnoreBegin(thr, pc);
@@ -1204,7 +1213,10 @@ TSAN_INTERCEPTOR(int, pthread_timedjoin_np, void *th, void **ret,
   SCOPED_INTERCEPTOR_RAW(pthread_timedjoin_np, th, ret, abstime);
   Tid tid;
   if (!ThreadConsumeTid(thr, pc, (uptr)th, &tid)) {
-    Report("ThreadSanitizer: pthread_timedjoin_np was called on thread %d but it is dead.\n", thr->tid);
+    Report(
+        "ThreadSanitizer: pthread_timedjoin_np was called on thread %d but it "
+        "is dead.\n",
+        thr->tid);
     return -errno_EINVAL;
   }
   ThreadIgnoreBegin(thr, pc);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl.h b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
index 03bd4d8de..ea0b2c163 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
@@ -563,7 +563,7 @@ Tid ThreadCreate(ThreadState *thr, uptr pc, uptr uid, bool detached);
 void ThreadStart(ThreadState *thr, Tid tid, ThreadID os_id,
                  ThreadType thread_type);
 void ThreadFinish(ThreadState *thr);
-bool ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid, Tid *tid_out);
+bool ThreadConsumeTid(ThreadState* thr, uptr pc, uptr uid, Tid* tid_out);
 void ThreadJoin(ThreadState *thr, uptr pc, Tid tid);
 void ThreadDetach(ThreadState *thr, uptr pc, Tid tid);
 void ThreadFinalize(ThreadState *thr);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp b/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
index fd33dde7e..a8cb1d385 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl_thread.cpp
@@ -301,7 +301,7 @@ struct ConsumeThreadContext {
   ThreadContextBase *tctx;
 };
 
-bool ThreadConsumeTid(ThreadState *thr, uptr pc, uptr uid, Tid *tid_out) {
+bool ThreadConsumeTid(ThreadState* thr, uptr pc, uptr uid, Tid* tid_out) {
   return ctx->thread_registry.ConsumeThreadUserId(uid, tid_out);
 }
 

@ghostway0 ghostway0 force-pushed the ghostway/tsan-156829-fix branch from 311b187 to f346915 Compare September 4, 2025 18:11

int main() {
pthread_t th;
int rc = pthread_create(&th, 0, fn, 0);
Copy link
Collaborator

@vitalybuka vitalybuka Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author of TSAN @dvyukov previously expressed strong opinion that it UB and tsan should crash here, and test above.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's UB here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should only be UB if not linked with -pthread? or am i wrong?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pthread_detach twice
and detach joined thread

@vitalybuka vitalybuka requested a review from dvyukov September 4, 2025 18:21
@ghostway0 ghostway0 force-pushed the ghostway/tsan-156829-fix branch from f346915 to 2b5dbfd Compare September 4, 2025 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[tsan] CHECK failed in thread sanitizer on ThreadCreate after finished thread was joined using pthread_clockjoin_np
4 participants