[libc-commits] [libc] [llvm] [libc] Unmap stack and exit detached threads via naked helper (PR #232780)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Fri Oct 16 07:48:12 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/232780

>From b3a50510276e92a6f23eb15635d46bd520774a2b Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 16 Oct 2026 13:53:16 +0100
Subject: [PATCH 1/2] [libc] Unmap stack and exit detached threads via naked
 helper

Relying on [[gnu::always_inline]] and a high optimisation flag to avoid
touching the stack after SYS_munmap is fragile.  Unoptimised builds
spill syscall return values and ErrorOr objects to the unmapped stack
frame between SYS_munmap and SYS_exit, causing SIGSEGV on thread exit.
This was noticed because the Bazel target for Linux threads lacked the
-O3 flag present in CMake, so unoptimised Bazel builds were already
exposed to this crash.

Add architecture-specific [[gnu::naked, noreturn]] unmap_and_exit
headers for x86_64, AArch64, and RISC-V that execute SYS_munmap and
SYS_exit without a stack frame.  Handle self-cleanup inside
cleanup_thread_resources so callers do not need to duplicate exit
handling after unmapping their own stack.  This also removes the need
for the forced optimisation flag on the Linux thread CMake target and
[[gnu::always_inline]] on free_stack and cleanup_thread_resources.

Assisted-by: Automated tooling, human reviewed.
---
 .../__support/threads/linux/CMakeLists.txt    | 19 ++++++-
 .../threads/linux/aarch64/unmap_and_exit.h    | 40 ++++++++++++++
 .../threads/linux/riscv/unmap_and_exit.h      | 40 ++++++++++++++
 libc/src/__support/threads/linux/thread.cpp   | 53 +++++++++++--------
 .../__support/threads/linux/unmap_and_exit.h  | 29 ++++++++++
 .../threads/linux/x86_64/unmap_and_exit.h     | 40 ++++++++++++++
 .../llvm-project-overlay/libc/BUILD.bazel     | 22 ++++++++
 7 files changed, 218 insertions(+), 25 deletions(-)
 create mode 100644 libc/src/__support/threads/linux/aarch64/unmap_and_exit.h
 create mode 100644 libc/src/__support/threads/linux/riscv/unmap_and_exit.h
 create mode 100644 libc/src/__support/threads/linux/unmap_and_exit.h
 create mode 100644 libc/src/__support/threads/linux/x86_64/unmap_and_exit.h

diff --git a/libc/src/__support/threads/linux/CMakeLists.txt b/libc/src/__support/threads/linux/CMakeLists.txt
index a106a6b13734a7..d56555e22939eb 100644
--- a/libc/src/__support/threads/linux/CMakeLists.txt
+++ b/libc/src/__support/threads/linux/CMakeLists.txt
@@ -14,6 +14,21 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_ARCHITECTURE}/tcb.h)
   )
 endif()
 
+if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_ARCHITECTURE}/unmap_and_exit.h)
+  add_header_library(
+    unmap_and_exit
+    HDRS
+      unmap_and_exit.h
+      ${LIBC_TARGET_ARCHITECTURE}/unmap_and_exit.h
+    DEPENDS
+      libc.hdr.types.size_t
+      libc.include.sys_syscall
+      libc.src.__support.macros.attributes
+      libc.src.__support.macros.config
+      libc.src.__support.macros.properties.architectures
+  )
+endif()
+
 add_header_library(
   futex_word_type
   HDRS
@@ -60,6 +75,7 @@ add_object_library(
     thread.cpp
   DEPENDS
     .futex_utils
+    .unmap_and_exit
     ${thread_tls_dep}
     libc.config.app_h
     libc.include.sys_syscall
@@ -75,6 +91,7 @@ add_object_library(
     libc.src.__support.CPP.string_view
     libc.src.__support.common
     libc.src.__support.error_or
+    libc.src.__support.macros.attributes
     libc.src.__support.OSUtil.linux.syscall_wrappers.clone
     libc.src.__support.OSUtil.linux.syscall_wrappers.close
     libc.src.__support.OSUtil.linux.syscall_wrappers.exit
@@ -93,8 +110,6 @@ add_object_library(
     libc.src.__support.threads.thread_common
     libc.src.__support.threads.thread_attributes
     libc.src.string.memory_utils.inline_memset
-  COMPILE_OPTIONS
-    ${libc_opt_high_flag}
 )
 
 add_header_library(
diff --git a/libc/src/__support/threads/linux/aarch64/unmap_and_exit.h b/libc/src/__support/threads/linux/aarch64/unmap_and_exit.h
new file mode 100644
index 00000000000000..d14a419371ec66
--- /dev/null
+++ b/libc/src/__support/threads/linux/aarch64/unmap_and_exit.h
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Implementation of AArch64 unmap_and_exit.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_AARCH64_UNMAP_AND_EXIT_H
+#define LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_AARCH64_UNMAP_AND_EXIT_H
+
+#include "hdr/types/size_t.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include <sys/syscall.h> // For SYS_munmap, SYS_exit
+
+namespace LIBC_NAMESPACE_DECL {
+
+// Unmap the calling thread's stack and exit the thread without touching the
+// stack between SYS_munmap and SYS_exit. In the AArch64 PCS, the first two
+// arguments (addr, size) arrive in x0 and x1, matching the syscall ABI for
+// SYS_munmap.
+[[gnu::naked, noreturn]] static void unmap_and_exit(void *, size_t) {
+  LIBC_INLINE_ASM("mov x8, %[sys_munmap]\n\t"
+                  "svc #0\n\t"
+                  "mov x0, #0\n\t"
+                  "mov x8, %[sys_exit]\n\t"
+                  "svc #0\n\t"
+                  "brk #0\n\t" : : [sys_munmap] "i"(SYS_munmap),
+                  [sys_exit] "i"(SYS_exit));
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_AARCH64_UNMAP_AND_EXIT_H
diff --git a/libc/src/__support/threads/linux/riscv/unmap_and_exit.h b/libc/src/__support/threads/linux/riscv/unmap_and_exit.h
new file mode 100644
index 00000000000000..ab099e86130186
--- /dev/null
+++ b/libc/src/__support/threads/linux/riscv/unmap_and_exit.h
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Implementation of RISC-V unmap_and_exit.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_RISCV_UNMAP_AND_EXIT_H
+#define LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_RISCV_UNMAP_AND_EXIT_H
+
+#include "hdr/types/size_t.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include <sys/syscall.h> // For SYS_munmap, SYS_exit
+
+namespace LIBC_NAMESPACE_DECL {
+
+// Unmap the calling thread's stack and exit the thread without touching the
+// stack between SYS_munmap and SYS_exit. In the RISC-V psABI, the first two
+// arguments (addr, size) arrive in a0 and a1, matching the syscall ABI for
+// SYS_munmap.
+[[gnu::naked, noreturn]] static void unmap_and_exit(void *, size_t) {
+  LIBC_INLINE_ASM("li a7, %[sys_munmap]\n\t"
+                  "ecall\n\t"
+                  "li a0, 0\n\t"
+                  "li a7, %[sys_exit]\n\t"
+                  "ecall\n\t"
+                  "unimp\n\t" : : [sys_munmap] "i"(SYS_munmap),
+                  [sys_exit] "i"(SYS_exit));
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_RISCV_UNMAP_AND_EXIT_H
diff --git a/libc/src/__support/threads/linux/thread.cpp b/libc/src/__support/threads/linux/thread.cpp
index c2d7413b2f9029..bc074b02ab7b46 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -35,9 +35,11 @@
 #include "src/__support/common.h"
 #include "src/__support/error_or.h"
 #include "src/__support/libc_errno.h" // For error macros
+#include "src/__support/macros/attributes.h"
 #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/linux/unmap_and_exit.h"
 #include "src/__support/threads/thread_attributes.h"
 #include "src/string/memory_utils/inline_memset.h"
 
@@ -118,11 +120,7 @@ LIBC_INLINE ErrorOr<void *> alloc_stack(size_t stacksize, size_t guardsize) {
   return stack;
 }
 
-// This must always be inlined as we may be freeing the calling threads stack in
-// which case a normal return from the top the stack would cause an invalid
-// memory read.
-[[gnu::always_inline]] LIBC_INLINE void
-free_stack(void *stack, size_t stacksize, size_t guardsize) {
+LIBC_INLINE void free_stack(void *stack, size_t stacksize, size_t guardsize) {
   uintptr_t stackaddr = reinterpret_cast<uintptr_t>(stack);
   stackaddr -= guardsize;
   stack = reinterpret_cast<void *>(stackaddr);
@@ -142,16 +140,33 @@ struct alignas(STACK_ALIGNMENT) StartArgs {
   void *arg;
 };
 
-// This must always be inlined as we may be freeing the calling threads stack in
-// which case a normal return from the top the stack would cause an invalid
-// memory read.
-[[gnu::always_inline]] LIBC_INLINE void
-cleanup_thread_resources(ThreadAttributes *attrib) {
-  // Cleanup the TLS before the stack as the TLS information is stored on
-  // the stack.
-  cleanup_tls(attrib->tls, attrib->tls_size);
-  if (attrib->owned_stack)
-    free_stack(attrib->stack, attrib->stacksize, attrib->guardsize);
+LIBC_INLINE void cleanup_thread_resources(ThreadAttributes *attrib) {
+  // Copy all needed fields from attrib before unmapping TLS or the stack, as
+  // attrib itself resides on the thread's stack.
+  void *stack = attrib->stack;
+  size_t stacksize = attrib->stacksize;
+  size_t guardsize = attrib->guardsize;
+  uintptr_t tls = attrib->tls;
+  uintptr_t tls_size = attrib->tls_size;
+  bool owned_stack = attrib->owned_stack;
+
+  if (attrib == current_thread().attrib) {
+    // Set the CLEAR_TID address to nullptr before unmapping the stack to
+    // prevent the kernel from signalling at a non-existent futex location.
+    LIBC_NAMESPACE::syscall_impl<long>(SYS_set_tid_address, 0);
+
+    cleanup_tls(tls, tls_size);
+    if (owned_stack) {
+      uintptr_t stackaddr = reinterpret_cast<uintptr_t>(stack) - guardsize;
+      unmap_and_exit(reinterpret_cast<void *>(stackaddr),
+                     stacksize + guardsize);
+    }
+    linux_syscalls::exit(0);
+  }
+
+  cleanup_tls(tls, tls_size);
+  if (owned_stack)
+    free_stack(stack, stacksize, guardsize);
 }
 
 static int start_thread(void *arg) {
@@ -515,14 +530,6 @@ void thread_exit(ThreadReturnValue retval, ThreadStyle style) {
     (void)linux_syscalls::rt_sigprocmask(SIG_BLOCK, &set, nullptr);
 
     cleanup_thread_resources(attrib);
-
-    // Set the CLEAR_TID address to nullptr to prevent the kernel
-    // from signalling at a non-existent futex location.
-    LIBC_NAMESPACE::syscall_impl<long>(SYS_set_tid_address, 0);
-    // Return value for detached thread should be unused. We need to avoid
-    // referencing `style` or `retval.*` because they may be stored on the stack
-    // and we have deallocated our stack!
-    linux_syscalls::exit(0);
   }
 
   if (style == ThreadStyle::POSIX)
diff --git a/libc/src/__support/threads/linux/unmap_and_exit.h b/libc/src/__support/threads/linux/unmap_and_exit.h
new file mode 100644
index 00000000000000..3fb0d4f94a221a
--- /dev/null
+++ b/libc/src/__support/threads/linux/unmap_and_exit.h
@@ -0,0 +1,29 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Defines Linux architecture-level unmap_and_exit dispatcher.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_UNMAP_AND_EXIT_H
+#define LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_UNMAP_AND_EXIT_H
+
+#include "src/__support/macros/properties/architectures.h"
+
+#if defined(LIBC_TARGET_ARCH_IS_X86_64)
+#include "src/__support/threads/linux/x86_64/unmap_and_exit.h"
+#elif defined(LIBC_TARGET_ARCH_IS_AARCH64)
+#include "src/__support/threads/linux/aarch64/unmap_and_exit.h"
+#elif defined(LIBC_TARGET_ARCH_IS_ANY_RISCV)
+#include "src/__support/threads/linux/riscv/unmap_and_exit.h"
+#else
+#error "Unsupported architecture for Linux unmap_and_exit"
+#endif
+
+#endif // LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_UNMAP_AND_EXIT_H
diff --git a/libc/src/__support/threads/linux/x86_64/unmap_and_exit.h b/libc/src/__support/threads/linux/x86_64/unmap_and_exit.h
new file mode 100644
index 00000000000000..a26d72343c7f07
--- /dev/null
+++ b/libc/src/__support/threads/linux/x86_64/unmap_and_exit.h
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Implementation of x86_64 unmap_and_exit.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_X86_64_UNMAP_AND_EXIT_H
+#define LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_X86_64_UNMAP_AND_EXIT_H
+
+#include "hdr/types/size_t.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include <sys/syscall.h> // For SYS_munmap, SYS_exit
+
+namespace LIBC_NAMESPACE_DECL {
+
+// Unmap the calling thread's stack and exit the thread without touching the
+// stack between SYS_munmap and SYS_exit. In the x86_64 System V ABI, the first
+// two arguments (addr, size) arrive in %rdi and %rsi, matching the syscall ABI
+// for SYS_munmap.
+[[gnu::naked, noreturn]] static void unmap_and_exit(void *, size_t) {
+  LIBC_INLINE_ASM("movl %[sys_munmap], %%eax\n\t"
+                  "syscall\n\t"
+                  "xorl %%edi, %%edi\n\t"
+                  "movl %[sys_exit], %%eax\n\t"
+                  "syscall\n\t"
+                  "ud2\n\t" : : [sys_munmap] "i"(SYS_munmap),
+                  [sys_exit] "i"(SYS_exit));
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_THREADS_LINUX_X86_64_UNMAP_AND_EXIT_H
diff --git a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
index 54846d24651432..121253096ca936 100644
--- a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
@@ -4718,6 +4718,27 @@ libc_support_library(
     }),
 )
 
+libc_support_library(
+    name = "__support_threads_linux_unmap_and_exit",
+    hdrs = ["src/__support/threads/linux/unmap_and_exit.h"],
+    full_build_only = True,
+    target_compatible_with = select({
+        "@platforms//os:linux": [],
+        "//conditions:default": ["@platforms//:incompatible"],
+    }),
+    textual_hdrs = [
+        "src/__support/threads/linux/aarch64/unmap_and_exit.h",
+        "src/__support/threads/linux/riscv/unmap_and_exit.h",
+        "src/__support/threads/linux/x86_64/unmap_and_exit.h",
+    ],
+    deps = [
+        ":__support_macros_attributes",
+        ":__support_macros_config",
+        ":__support_macros_properties_architectures",
+        ":types_size_t",
+    ],
+)
+
 libc_support_library(
     name = "__support_threads_linux_thread",
     srcs = ["src/__support/threads/linux/thread.cpp"],
@@ -4759,6 +4780,7 @@ libc_support_library(
         ":__support_threads_linux_futex_utils",
         ":__support_threads_linux_futex_word_type",
         ":__support_threads_linux_tls",
+        ":__support_threads_linux_unmap_and_exit",
         ":__support_threads_mutex",
         ":__support_threads_thread_attributes",
         ":__support_threads_thread_headers",

>From 155bad3e4d5cb8fb41491396583c0004cffded3c Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 16 Oct 2026 15:47:57 +0100
Subject: [PATCH 2/2] [libc] Guard unmap_and_exit CMake dependency on
 unsupported arches

On 32-bit ARM (armhf), OSUtil is present so CMake evaluates the Linux
thread target even though Linux threads are not supported on armhf.
Guard the .unmap_and_exit dependency with a variable matching
thread_tls_dep so CMake generation succeeds when unmap_and_exit.h does
not exist for the target architecture.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/__support/threads/linux/CMakeLists.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/libc/src/__support/threads/linux/CMakeLists.txt b/libc/src/__support/threads/linux/CMakeLists.txt
index d56555e22939eb..f1f227b06cefe9 100644
--- a/libc/src/__support/threads/linux/CMakeLists.txt
+++ b/libc/src/__support/threads/linux/CMakeLists.txt
@@ -27,6 +27,7 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_ARCHITECTURE}/unmap_and_exit
       libc.src.__support.macros.config
       libc.src.__support.macros.properties.architectures
   )
+  set(thread_unmap_and_exit_dep .unmap_and_exit)
 endif()
 
 add_header_library(
@@ -75,7 +76,7 @@ add_object_library(
     thread.cpp
   DEPENDS
     .futex_utils
-    .unmap_and_exit
+    ${thread_unmap_and_exit_dep}
     ${thread_tls_dep}
     libc.config.app_h
     libc.include.sys_syscall



More information about the libc-commits mailing list