[llvm-branch-commits] [compiler-rt] [sanitizer] Clear pending dlerror on thread exit before unregistering (PR #228930)

Vitaly Buka via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Sun Oct 4 13:46:18 PDT 2026


https://github.com/vitalybuka updated https://github.com/llvm/llvm-project/pull/228930

>From abdccf38d311320eedd7c65b450b5c02c7f4f801 Mon Sep 17 00:00:00 2001
From: Vitaly Buka <vitalybuka at google.com>
Date: Sun, 4 Oct 2026 13:45:05 -0700
Subject: [PATCH] [sanitizer] Clear pending dlerror on thread exit before
 unregistering

Starting with glibc 2.34, failed dlfcn calls store a per-thread error
struct in TLS (__libc_dlerror_result) instead of a pthread_key_t, and
free it in __libc_thread_freeres() after all pthread TSD destructors
have completed.

Because ASan, LSan, and HWASan unregister the exiting thread in their
pthread TSD destructors during __nptl_deallocate_tsd(), LSan stops
scanning that thread's TLS before __libc_thread_freeres() frees
__libc_dlerror_result. If a leak check runs in that window (for example,
when the main thread exits while a detached worker thread is finishing)
and libc debug symbols are not installed to match the default *dlerror*
suppression on _dlerror_run, LSan reports a false positive leak from
dlsym/dlopen.

This manifests as flaky LSan leaks on `LLVM :: ExecutionEngine/JITLink/x86-64/`
tests on `sanitizer-*-linux-bootstrap-asan` and `sanitizer-x86_64-linux-fast`
buildbots, exposed after re-enabling `ExecutionEngine` tests under ASan in
https://github.com/llvm/llvm-zorg/pull/971 (reverting
https://github.com/llvm/llvm-zorg/pull/823; see #186534).

Call `if (dlerror()) dlerror();` before unregistering the exiting thread
so glibc frees its per-thread dlerror buffer while the thread is still
tracked.

Assisted-by: Gemini

Pull Request: https://github.com/llvm/llvm-project/pull/228930
---
 compiler-rt/lib/asan/asan_thread.cpp          |  5 +-
 compiler-rt/lib/hwasan/hwasan_linux.cpp       |  2 +
 compiler-rt/lib/lsan/lsan_thread.cpp          |  6 +-
 .../lib/sanitizer_common/sanitizer_dl.cpp     | 13 ++++
 .../lib/sanitizer_common/sanitizer_dl.h       |  4 ++
 .../TestCases/Linux/detached_thread_dlsym.c   | 63 +++++++++++++++++++
 6 files changed, 91 insertions(+), 2 deletions(-)
 create mode 100644 compiler-rt/test/lsan/TestCases/Linux/detached_thread_dlsym.c

diff --git a/compiler-rt/lib/asan/asan_thread.cpp b/compiler-rt/lib/asan/asan_thread.cpp
index ec96ec9b8082bc1..a7bdc3a2587431e 100644
--- a/compiler-rt/lib/asan/asan_thread.cpp
+++ b/compiler-rt/lib/asan/asan_thread.cpp
@@ -19,6 +19,7 @@
 #include "asan_stack.h"
 #include "lsan/lsan_common.h"
 #include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_dl.h"
 #include "sanitizer_common/sanitizer_placement_new.h"
 #include "sanitizer_common/sanitizer_stackdepot.h"
 #include "sanitizer_common/sanitizer_thread_history.h"
@@ -117,8 +118,10 @@ void AsanThread::GetStartData(void *out, uptr out_size) const {
 void AsanThread::TSDDtor(void *tsd) {
   AsanThreadContext *context = (AsanThreadContext *)tsd;
   VReport(1, "T%d TSDDtor\n", context->tid);
-  if (context->thread)
+  if (context->thread) {
+    ClearDlerror();
     context->thread->Destroy();
+  }
 }
 
 void AsanThread::Destroy() {
diff --git a/compiler-rt/lib/hwasan/hwasan_linux.cpp b/compiler-rt/lib/hwasan/hwasan_linux.cpp
index 716a8d47ae1924c..351306cc761edbc 100644
--- a/compiler-rt/lib/hwasan/hwasan_linux.cpp
+++ b/compiler-rt/lib/hwasan/hwasan_linux.cpp
@@ -37,6 +37,7 @@
 #  include "hwasan_thread.h"
 #  include "hwasan_thread_list.h"
 #  include "sanitizer_common/sanitizer_common.h"
+#  include "sanitizer_common/sanitizer_dl.h"
 #  include "sanitizer_common/sanitizer_procmaps.h"
 #  include "sanitizer_common/sanitizer_stackdepot.h"
 
@@ -599,6 +600,7 @@ extern "C" void __hwasan_thread_exit() {
     // Bionic already calls __hwasan_thread_exit with blocked signals.
     if (SANITIZER_GLIBC)
       BlockSignals();
+    ClearDlerror();
     hwasanThreadList().ReleaseThread(t);
   }
 }
diff --git a/compiler-rt/lib/lsan/lsan_thread.cpp b/compiler-rt/lib/lsan/lsan_thread.cpp
index 22eb9ee7680fbf5..d40fa3b720ea193 100644
--- a/compiler-rt/lib/lsan/lsan_thread.cpp
+++ b/compiler-rt/lib/lsan/lsan_thread.cpp
@@ -17,6 +17,7 @@
 #include "lsan_allocator.h"
 #include "lsan_common.h"
 #include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_dl.h"
 #include "sanitizer_common/sanitizer_placement_new.h"
 #include "sanitizer_common/sanitizer_thread_history.h"
 #include "sanitizer_common/sanitizer_thread_registry.h"
@@ -71,7 +72,10 @@ void ThreadContextLsanBase::ThreadStart(u32 tid, ThreadID os_id,
   thread_registry->StartThread(tid, os_id, thread_type, arg);
 }
 
-void ThreadFinish() { thread_registry->FinishThread(GetCurrentThreadId()); }
+void ThreadFinish() {
+  ClearDlerror();
+  thread_registry->FinishThread(GetCurrentThreadId());
+}
 
 void EnsureMainThreadIDIsCorrect() {
   if (GetCurrentThreadId() == kMainTid)
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_dl.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_dl.cpp
index b18ce648e74e9c7..3e944af639b0a87 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_dl.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_dl.cpp
@@ -45,4 +45,17 @@ char* DladdrElfHeaderBase(void* ld, char* addr) {
   return addr;
 }
 
+void ClearDlerror() {
+#if SANITIZER_GLIBC && !SANITIZER_GO
+  // Starting with glibc 2.34, failed dlfcn calls store a per-thread error
+  // struct in TLS (__libc_dlerror_result) instead of a pthread_key_t, and free
+  // it in __libc_thread_freeres() after pthread TSD destructors have already
+  // unregistered the thread from the sanitizer. Clear dlerror() while the
+  // thread is still tracked so that LSan does not report the pending error
+  // buffer as a leak if a leak check runs before __libc_thread_freeres().
+  if (dlerror())
+    dlerror();
+#endif
+}
+
 }  // namespace __sanitizer
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_dl.h b/compiler-rt/lib/sanitizer_common/sanitizer_dl.h
index b98e4dfe48409c0..f791f6e9b23a65b 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_dl.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_dl.h
@@ -25,6 +25,10 @@ const char* DladdrSelfFName(void);
 // into account.
 char* DladdrElfHeaderBase(void* ld, char* addr);
 
+// Clears any pending dlerror() message on the current thread so that glibc
+// frees its per-thread error buffer before the thread is unregistered.
+void ClearDlerror();
+
 }  // namespace __sanitizer
 
 #endif  // SANITIZER_DL_H
diff --git a/compiler-rt/test/lsan/TestCases/Linux/detached_thread_dlsym.c b/compiler-rt/test/lsan/TestCases/Linux/detached_thread_dlsym.c
new file mode 100644
index 000000000000000..a0b801f1ca0f18c
--- /dev/null
+++ b/compiler-rt/test/lsan/TestCases/Linux/detached_thread_dlsym.c
@@ -0,0 +1,63 @@
+// Test that TLS allocations (such as glibc's internal dlerror buffer from a
+// failed dlsym call, freed in __libc_thread_freeres) on a dying detached
+// thread are not falsely reported as leaks if leak checking runs after the
+// sanitizer's TSD destructor has executed.
+//
+// Disable symbolization so that even if libc debug symbols are installed,
+// LSan's default `leak:*dlerror*` suppression cannot match the non-exported
+// `_dlerror_run` frame and mask the leak.
+// RUN: %clang_lsan %s -ldl -pthread -o %t && %env_lsan_opts=symbolize=0 %run %t
+
+#define _GNU_SOURCE
+#include <assert.h>
+#include <dlfcn.h>
+#include <limits.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <unistd.h>
+
+static pthread_key_t key;
+static atomic_int ready = 0;
+
+static void key_destructor(void *arg) {
+  uintptr_t iter = (uintptr_t)arg;
+  if (iter > 1) {
+    int res = pthread_setspecific(key, (void *)(iter - 1));
+    assert(res == 0);
+    return;
+  }
+  // On the last iteration (PTHREAD_DESTRUCTOR_ITERATIONS), the sanitizer's TSD
+  // destructor (registered earlier with a lower key index) has already run,
+  // while glibc's __libc_thread_freeres() has not yet freed the dlerror buffer.
+  atomic_store(&ready, 1);
+  sleep(10);
+}
+
+static void *thread_func(void *arg) {
+  dlsym(RTLD_DEFAULT, "nonexistent_symbol_12345");
+  int res = pthread_setspecific(key, (void *)PTHREAD_DESTRUCTOR_ITERATIONS);
+  assert(res == 0);
+  return NULL;
+}
+
+int main(void) {
+  int res = pthread_key_create(&key, key_destructor);
+  assert(res == 0);
+
+  pthread_attr_t attr;
+  res = pthread_attr_init(&attr);
+  assert(res == 0);
+  res = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+  assert(res == 0);
+
+  pthread_t th;
+  res = pthread_create(&th, &attr, thread_func, NULL);
+  assert(res == 0);
+  pthread_attr_destroy(&attr);
+
+  while (!atomic_load(&ready))
+    sched_yield();
+
+  return 0;
+}



More information about the llvm-branch-commits mailing list