[libc-commits] [libc] [libc] Make it possible to join the main thread (PR #221177)

Pavel Labath via libc-commits libc-commits at lists.llvm.org
Fri Sep 4 02:22:16 PDT 2026


https://github.com/labath created https://github.com/llvm/llvm-project/pull/221177

Joining the main thread currently crashes because its ThreadAttributes has a null platform_data pointer, causing Thread::wait() to dereference a null futex.

While this is not a frequently used feature, it is supported by other implementations, and I believe it is required by POSIX (the exec page says that the main thread is created in a joinable state, and neither pthread_join nor pthread_exit mention them not working on the main thread).

This patch sets up the main thread attributes during startup:
- allocate a static futex for the main thread's clear_tid and point platform_data to it
- invoke set_tid_address to have the kernel clear the futex and wake waiters on thread termination
- mark the main thread as joinable (it was previously defaulting to detached)

The test for this functionality exposed the bug where the thread return value is not preserved (and therefore not passed to pthread_join) for threads exiting via pthread_exit, so I fix that as well.

This patch required accessing the CLEAR_TID_VALUE constant from multiple files, but I couldn't find a suitable header to place it in. Instead of creating a new one, I just delete the constant, as we don't really care about its value -- only that it is not zero.

Assisted-by: Gemini

>From 27dd41116dd9d6217e48c63c8a2351a30ddecc77 Mon Sep 17 00:00:00 2001
From: Pavel Labath <pavel at labath.sk>
Date: Fri, 4 Sep 2026 08:41:35 +0000
Subject: [PATCH] [libc] Make it possible to join the main thread

Joining the main thread currently crashes because its ThreadAttributes
has a null platform_data pointer, causing Thread::wait() to dereference
a null futex.

While this is not a frequently used feature, it is supported by other
implementations, and I believe it is required by POSIX (the exec page
says that the main thread is created in a joinable state, and neither
pthread_join nor pthread_exit mention them not working on the main
thread).

This patch sets up the main thread attributes during startup:
- allocate a static futex for the main thread's clear_tid and point
  platform_data to it
- invoke set_tid_address to have the kernel clear the futex and wake
  waiters on thread termination
- mark the main thread as joinable (it was previously defaulting to
  detached)

The test for this functionality exposed the bug where the thread return
value is not preserved (and therefore not passed to pthread_join) for
threads exiting via pthread_exit, so I fix that as well.

This patch required accessing the CLEAR_TID_VALUE constant from multiple
files, but I couldn't find a suitable header to place it in. Instead of
creating a new one, I just delete the constant, as we don't really care
about its value -- only that it is not zero.

Assisted-by: Gemini
---
 libc/src/__support/threads/linux/thread.cpp   | 23 +++++-----
 libc/startup/linux/CMakeLists.txt             |  1 +
 libc/startup/linux/do_start.cpp               |  6 +++
 .../integration/src/pthread/CMakeLists.txt    | 15 ++++++
 .../src/pthread/pthread_join_main_test.cpp    | 46 +++++++++++++++++++
 5 files changed, 79 insertions(+), 12 deletions(-)
 create mode 100644 libc/test/integration/src/pthread/pthread_join_main_test.cpp

diff --git a/libc/src/__support/threads/linux/thread.cpp b/libc/src/__support/threads/linux/thread.cpp
index 231b7cf2903ca..7e91125823c24 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -27,6 +27,8 @@
 #include "src/__support/libc_errno.h" // For error macros
 #include "src/__support/macros/config.h"
 #include "src/__support/threads/linux/futex_utils.h" // For FutexWordType
+#include "src/__support/threads/linux/futex_word.h"
+#include "src/__support/threads/thread_attributes.h"
 
 #ifdef LIBC_TARGET_ARCH_IS_AARCH64
 #include <arm_acle.h>
@@ -44,7 +46,6 @@
 namespace LIBC_NAMESPACE_DECL {
 
 static constexpr size_t NAME_SIZE_MAX = 16; // Includes the null terminator
-static constexpr uint32_t CLEAR_TID_VALUE = 0xABCD1234;
 static constexpr unsigned CLONE_SYSCALL_FLAGS =
     CLONE_VM        // Share the memory space with the parent.
     | CLONE_FS      // Share the file system with the parent.
@@ -184,15 +185,11 @@ cleanup_thread_resources(ThreadAttributes *attrib) {
   attrib->atexit_callback_mgr = internal::get_thread_atexit_callback_mgr();
 
   if (attrib->style == ThreadStyle::POSIX) {
-    attrib->retval.posix_retval =
-        start_args->runner.posix_runner(start_args->arg);
-    thread_exit(ThreadReturnValue(attrib->retval.posix_retval),
-                ThreadStyle::POSIX);
+    ThreadReturnValue retval = start_args->runner.posix_runner(start_args->arg);
+    thread_exit(retval, ThreadStyle::POSIX);
   } else {
-    attrib->retval.stdc_retval =
-        start_args->runner.stdc_runner(start_args->arg);
-    thread_exit(ThreadReturnValue(attrib->retval.stdc_retval),
-                ThreadStyle::STDC);
+    ThreadReturnValue retval = start_args->runner.stdc_runner(start_args->arg);
+    thread_exit(retval, ThreadStyle::STDC);
   }
 }
 
@@ -291,7 +288,7 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
 
   auto clear_tid = reinterpret_cast<Futex *>(
       adjusted_stack + sizeof(StartArgs) + sizeof(ThreadAttributes));
-  clear_tid->set(CLEAR_TID_VALUE);
+  clear_tid->set(1);
   attrib->platform_data = clear_tid;
 
   // The clone syscall takes arguments in an architecture specific order.
@@ -399,8 +396,9 @@ void Thread::wait() {
   auto *clear_tid = reinterpret_cast<Futex *>(attrib->platform_data);
   // We cannot do a FUTEX_WAIT_PRIVATE here as the kernel does a
   // FUTEX_WAKE and not a FUTEX_WAKE_PRIVATE.
-  while (clear_tid->load() != 0)
-    clear_tid->wait(CLEAR_TID_VALUE, cpp::nullopt, true);
+  FutexWordType clear_tid_value;
+  while ((clear_tid_value = clear_tid->load()) != 0)
+    clear_tid->wait(clear_tid_value, cpp::nullopt, true);
 }
 
 bool Thread::operator==(const Thread &thread) const {
@@ -517,6 +515,7 @@ ErrorOr<SchedParameters> Thread::getschedparam() const {
 
 void thread_exit(ThreadReturnValue retval, ThreadStyle style) {
   auto attrib = current_thread().attrib;
+  attrib->retval = retval;
 
   // The very first thing we do is to call the thread's atexit callbacks.
   // These callbacks could be the ones registered by the language runtimes,
diff --git a/libc/startup/linux/CMakeLists.txt b/libc/startup/linux/CMakeLists.txt
index 11aa97ef97c40..e6568324a6c78 100644
--- a/libc/startup/linux/CMakeLists.txt
+++ b/libc/startup/linux/CMakeLists.txt
@@ -122,6 +122,7 @@ add_object_library(
     libc.src.__support.OSUtil.linux.auxv
     libc.src.__support.OSUtil.osutil
     libc.src.__support.threads.thread
+    libc.src.__support.threads.linux.futex_utils
     libc.src.__support.macros.config
     libc.startup.linux.${LIBC_TARGET_ARCHITECTURE}.irelative
     libc.src.link._r_debug
diff --git a/libc/startup/linux/do_start.cpp b/libc/startup/linux/do_start.cpp
index 410cbe8e12a48..5ff5e73c4649a 100644
--- a/libc/startup/linux/do_start.cpp
+++ b/libc/startup/linux/do_start.cpp
@@ -18,6 +18,7 @@
 #include "src/__support/OSUtil/linux/auxv.h"
 #include "src/__support/OSUtil/syscall.h"
 #include "src/__support/macros/config.h"
+#include "src/__support/threads/linux/futex_utils.h"
 #include "src/__support/threads/thread.h"
 #include "src/errno/program_invocation_name.h"
 #include "src/errno/program_invocation_short_name.h"
@@ -76,6 +77,7 @@ static void call_fini_array_callbacks() {
 }
 
 static ThreadAttributes main_thread_attrib;
+static Futex main_thread_clear_tid(1);
 static TLSDescriptor tls;
 
 [[noreturn]] void do_start() {
@@ -83,6 +85,10 @@ static TLSDescriptor tls;
   if (tid <= 0)
     syscall_impl<long>(SYS_exit, 1);
   main_thread_attrib.tid = static_cast<int>(tid);
+  main_thread_attrib.platform_data = &main_thread_clear_tid;
+  main_thread_attrib.detach_state =
+      static_cast<uint32_t>(DetachState::JOINABLE);
+  syscall_impl<long>(SYS_set_tid_address, &main_thread_clear_tid.val);
 
   // After the argv array, is a 8-byte long NULL value before the array of env
   // values. The end of the env values is marked by another 8-byte long NULL
diff --git a/libc/test/integration/src/pthread/CMakeLists.txt b/libc/test/integration/src/pthread/CMakeLists.txt
index 19d02bbc3f0c4..ced0782736ef8 100644
--- a/libc/test/integration/src/pthread/CMakeLists.txt
+++ b/libc/test/integration/src/pthread/CMakeLists.txt
@@ -259,6 +259,21 @@ add_integration_test(
     libc.src.pthread.pthread_self
 )
 
+add_integration_test(
+  pthread_join_main_test
+  SUITE
+    libc-pthread-integration-tests
+  SRCS
+    pthread_join_main_test.cpp
+  DEPENDS
+    libc.include.pthread
+    libc.src.pthread.pthread_create
+    libc.src.pthread.pthread_exit
+    libc.src.pthread.pthread_join
+    libc.src.pthread.pthread_self
+    libc.src.stdlib.exit
+)
+
 add_integration_test(
   pthread_create_test
   SUITE
diff --git a/libc/test/integration/src/pthread/pthread_join_main_test.cpp b/libc/test/integration/src/pthread/pthread_join_main_test.cpp
new file mode 100644
index 0000000000000..2b4ee5446bc5c
--- /dev/null
+++ b/libc/test/integration/src/pthread/pthread_join_main_test.cpp
@@ -0,0 +1,46 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Test for pthread_join on the main thread.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/pthread/pthread_create.h"
+#include "src/pthread/pthread_exit.h"
+#include "src/pthread/pthread_join.h"
+#include "src/pthread/pthread_self.h"
+#include "src/stdlib/exit.h"
+#include "test/IntegrationTest/test.h"
+
+#include <pthread.h>
+
+static void *const RETVAL = reinterpret_cast<void *>(0xdead);
+
+static pthread_t main_thread;
+
+static void *worker_func(void *) {
+  void *retval = nullptr;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_join(main_thread, &retval), 0);
+  ASSERT_EQ(retval, RETVAL);
+
+  LIBC_NAMESPACE::exit(0);
+  __builtin_unreachable();
+}
+
+TEST_MAIN() {
+  main_thread = LIBC_NAMESPACE::pthread_self();
+
+  pthread_t worker_thread;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&worker_thread, nullptr, worker_func,
+                                           nullptr),
+            0);
+
+  LIBC_NAMESPACE::pthread_exit(RETVAL);
+  __builtin_unreachable();
+}



More information about the libc-commits mailing list