[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
Wed Sep 16 02:55:48 PDT 2026
https://github.com/labath updated https://github.com/llvm/llvm-project/pull/221177
>From 8335bd9e45d3ab6eb3ad075b5867ea1f55ba934a 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 | 22 +++++----
libc/startup/linux/CMakeLists.txt | 1 +
libc/startup/linux/do_start.cpp | 6 +++
.../integration/src/pthread/CMakeLists.txt | 15 +++++++
.../src/pthread/pthread_getattr_np_test.cpp | 2 +-
.../src/pthread/pthread_join_main_test.cpp | 45 +++++++++++++++++++
6 files changed, 78 insertions(+), 13 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 3dcb6fd6da6ac8..efbdc4a45d3734 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -29,6 +29,7 @@
#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
@@ -48,7 +49,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.
@@ -186,15 +186,11 @@ cleanup_thread_resources(ThreadAttributes *attrib) {
auto *attrib = start_args->thread_attrib;
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);
}
}
@@ -297,7 +293,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;
get_tcb(tls.tp)->attrib = attrib;
@@ -407,8 +403,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 {
@@ -561,6 +558,7 @@ ErrorOr<void> Thread::kill(int sig) {
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 6fc59020ab0435..e798b00e8819ae 100644
--- a/libc/startup/linux/CMakeLists.txt
+++ b/libc/startup/linux/CMakeLists.txt
@@ -123,6 +123,7 @@ add_object_library(
libc.src.__support.OSUtil.osutil
libc.src.__support.threads.tcb
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 a9b7168528dd42..7de1866a35145b 100644
--- a/libc/startup/linux/do_start.cpp
+++ b/libc/startup/linux/do_start.cpp
@@ -19,6 +19,7 @@
#include "src/__support/OSUtil/syscall.h"
#include "src/__support/macros/config.h"
#include "src/__support/threads/tcb.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"
@@ -77,6 +78,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() {
@@ -84,6 +86,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 33794387a47082..abeadc4be1c9a2 100644
--- a/libc/test/integration/src/pthread/CMakeLists.txt
+++ b/libc/test/integration/src/pthread/CMakeLists.txt
@@ -272,6 +272,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_getattr_np_test.cpp b/libc/test/integration/src/pthread/pthread_getattr_np_test.cpp
index ccba2102acdac8..5833856cba0c80 100644
--- a/libc/test/integration/src/pthread/pthread_getattr_np_test.cpp
+++ b/libc/test/integration/src/pthread/pthread_getattr_np_test.cpp
@@ -82,7 +82,7 @@ static void test_main_thread() {
PthreadAttrValues values;
values.populate_from(LIBC_NAMESPACE::pthread_self());
- ASSERT_EQ(values.detachstate, static_cast<int>(PTHREAD_CREATE_DETACHED));
+ ASSERT_EQ(values.detachstate, static_cast<int>(PTHREAD_CREATE_JOINABLE));
ASSERT_NE(values.stackaddr, static_cast<void *>(nullptr));
ASSERT_EQ(values.stacksize, static_cast<size_t>(PTHREAD_STACK_DYNAMIC_NP));
ASSERT_EQ(reinterpret_cast<uintptr_t>(values.stackaddr) % pagesize(),
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 00000000000000..73c460246836e2
--- /dev/null
+++ b/libc/test/integration/src/pthread/pthread_join_main_test.cpp
@@ -0,0 +1,45 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "include/llvm-libc-macros/pthread-macros.h"
+#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"
+
+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