This is an automated email from the ASF dual-hosted git repository.

zclllyybb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new dc70115a432 [Enhancement](BE) Make print stack API more stable (#64093)
dc70115a432 is described below

commit dc70115a432093bf4124be740e4325024e2a8ea3
Author: Jover Zoeng <[email protected]>
AuthorDate: Mon Jul 6 16:59:30 2026 +0800

    [Enhancement](BE) Make print stack API more stable (#64093)
    
    ### What problem does this PR solve?
    
    Issue Number: close #62497
    
    follow up https://github.com/apache/doris/pull/64454
    
    #### Problem Summary:
    
    This PR adds a safer BE print stack API based on libunwind
    signal-context unwinding.
    
    Stack unwinding may call `dl_iterate_phdr()` to find DWARF/FDE metadata.
    In signal-based stack capture, jemalloc profiling, or concurrent
    `dlopen()` / `dlclose()`, re-entering glibc's loader-lock path may
    deadlock or become unsafe.
    
    #### Solution
    
    Route GNU libunwind's PHDR lookup through Doris' lock-free PHDR cache.
    
    The signal handler unwinds from the kernel-provided signal context,
    while libunwind reads loaded-object metadata from Doris' PHDR snapshot
    instead of calling glibc `dl_iterate_phdr()` directly.
    
    Normal code paths still use the original live `dl_iterate_phdr()`
    behavior.
    
    The research project is at
    https://github.com/JoverZhang/Doris-BE-Print-Stack-API-Research
    
    Co-authored-by: zhaochangle <[email protected]>
---
 be/CMakeLists.txt                                  |   5 +-
 be/src/common/config.cpp                           |   1 -
 be/src/common/config.h                             |   2 -
 be/src/common/phdr_cache.cpp                       | 237 ++++++++++++--
 be/src/common/phdr_cache.h                         |  59 +++-
 be/src/common/stack_trace.cpp                      |  16 +-
 be/src/common/stack_trace.h                        |   5 +-
 be/src/common/symbol_index.cpp                     | 230 ++++++++++---
 be/src/service/doris_main.cpp                      |  20 +-
 .../service/http/action/be_thread_stack_action.cpp | 364 +++++----------------
 be/src/util/dynamic_util.cpp                       |  17 +
 be/src/util/libjvm_loader.cpp                      |  30 +-
 be/src/util/stack_util.cpp                         |  55 ----
 be/src/util/stack_util.h                           |  34 +-
 be/test/CMakeLists.txt                             |  13 +
 be/test/common/phdr_cache_test.cpp                 | 103 ++++++
 be/test/common/phdr_cache_test_dso.cpp             |  20 ++
 .../service/http/be_thread_stack_action_test.cpp   |  50 ++-
 be/test/testutil/run_all_tests.cpp                 |   1 +
 build.sh                                           |  10 -
 cloud/CMakeLists.txt                               |   7 +
 cloud/cmake/thirdparty.cmake                       |   3 +
 run-be-ut.sh                                       |   9 -
 thirdparty/build-thirdparty.sh                     |  38 ++-
 thirdparty/download-thirdparty.sh                  |  14 +
 .../patches/libunwind-1.6.2-doris-phdr-cache.patch |  31 ++
 26 files changed, 866 insertions(+), 508 deletions(-)

diff --git a/be/CMakeLists.txt b/be/CMakeLists.txt
index 4e7482ddc32..fa7e716df92 100644
--- a/be/CMakeLists.txt
+++ b/be/CMakeLists.txt
@@ -76,12 +76,13 @@ add_definitions(-DGLOG_CUSTOM_PREFIX_SUPPORT)
 # Options
 option(GLIBC_COMPATIBILITY "Enable compatibility with older glibc libraries." 
ON)
 option(USE_LIBCPP "Use libc++" OFF)
-option(USE_UNWIND "Use libunwind" ON)
 option(USE_JEMALLOC "Use jemalloc" ON)
 if (OS_MACOSX)
     set(GLIBC_COMPATIBILITY OFF)
     set(USE_LIBCPP ON)
     set(USE_UNWIND OFF)
+else()
+    set(USE_UNWIND ON)
 endif()
 
 if (DISPLAY_BUILD_TIME)
@@ -456,7 +457,7 @@ if (USE_JEMALLOC)
     add_definitions(-DUSE_JEMALLOC)
 endif()
 
-# Compile with libunwind
+# libunwind symbolization uses DWARF aranges for fast address-to-line lookup.
 if (USE_UNWIND)
     add_definitions(-DUSE_UNWIND)
     if (COMPILER_CLANG)
diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp
index 0638b1a244a..dfa7783075f 100644
--- a/be/src/common/config.cpp
+++ b/be/src/common/config.cpp
@@ -1359,7 +1359,6 @@ DEFINE_mString(kerberos_krb5_conf_path, "/etc/krb5.conf");
 // JDK-8153057: avoid StackOverflowError thrown from the 
UncaughtExceptionHandler in thread "process reaper"
 DEFINE_mBool(jdk_process_reaper_use_default_stack_size, "true");
 
-DEFINE_mString(get_stack_trace_tool, "libunwind");
 DEFINE_mString(dwarf_location_info_mode, "FAST");
 DEFINE_mBool(enable_address_sanitizers_with_stack_trace, "true");
 
diff --git a/be/src/common/config.h b/be/src/common/config.h
index df19ec0fef9..cb174ff2fa4 100644
--- a/be/src/common/config.h
+++ b/be/src/common/config.h
@@ -1411,8 +1411,6 @@ DECLARE_mString(kerberos_krb5_conf_path);
 // JDK-8153057: avoid StackOverflowError thrown from the 
UncaughtExceptionHandler in thread "process reaper"
 DECLARE_mBool(jdk_process_reaper_use_default_stack_size);
 
-// Values include `none`, `glog`, `boost`, `glibc`, `libunwind`
-DECLARE_mString(get_stack_trace_tool);
 DECLARE_mBool(enable_address_sanitizers_with_stack_trace);
 
 // DISABLED: Don't resolve location info.
diff --git a/be/src/common/phdr_cache.cpp b/be/src/common/phdr_cache.cpp
index 4ab863a462b..afc2cfb5ab7 100644
--- a/be/src/common/phdr_cache.cpp
+++ b/be/src/common/phdr_cache.cpp
@@ -18,6 +18,8 @@
 // https://github.com/ClickHouse/ClickHouse/blob/master/src/base/phdr_cache.cpp
 // and modified by Doris
 
+#include "common/phdr_cache.h"
+
 #if defined(__clang__)
 #pragma clang diagnostic ignored "-Wreserved-identifier"
 #endif
@@ -47,17 +49,36 @@
 #include <dlfcn.h>
 #include <link.h>
 
+#include <algorithm>
+#include <array>
 #include <atomic>
 #include <cstddef>
+#include <cstring>
+#include <limits>
+#include <memory>
+#include <mutex>
 #include <stdexcept>
+#include <string>
+#include <utility>
 #include <vector>
 
+#if defined(USE_UNWIND) && USE_UNWIND
+#ifndef UNW_LOCAL_ONLY
+#define UNW_LOCAL_ONLY
+#endif
+#include <libunwind.h>
+#endif
+
 namespace {
 
 // This is adapted from
 // https://github.com/scylladb/seastar/blob/master/core/exception_hacks.hh
 // https://github.com/scylladb/seastar/blob/master/core/exception_hacks.cc
 
+constexpr size_t MAX_PHDR_CACHE_LOADED_OBJECTS = 4096;
+constexpr size_t MAX_PHDR_CACHE_PROGRAM_HEADERS = 128;
+constexpr size_t MAX_PHDR_CACHE_OBJECT_NAME = 4096;
+
 using DLIterateFunction = int (*)(int (*callback)(dl_phdr_info* info, size_t 
size, void* data),
                                   void* data);
 
@@ -69,74 +90,226 @@ DLIterateFunction getOriginalDLIteratePHDR() {
     return reinterpret_cast<DLIterateFunction>(func);
 }
 
-using PHDRCache = std::vector<dl_phdr_info>;
+struct RawPHDRCacheEntry {
+    dl_phdr_info info {};
+    std::array<ElfW(Phdr), MAX_PHDR_CACHE_PROGRAM_HEADERS> phdrs {};
+    size_t phdr_count = 0;
+    std::array<char, MAX_PHDR_CACHE_OBJECT_NAME> name {};
+    uintptr_t address_begin = std::numeric_limits<uintptr_t>::max();
+    uintptr_t address_end = 0;
+};
+
+struct RawPHDRCacheSnapshot {
+    std::array<RawPHDRCacheEntry, MAX_PHDR_CACHE_LOADED_OBJECTS> entries {};
+    size_t size = 0;
+    bool overflow = false;
+    bool phdr_truncated = false;
+    bool name_truncated = false;
+};
+
+struct PHDRCacheEntry {
+    dl_phdr_info info {};
+    std::vector<ElfW(Phdr)> phdrs;
+    std::string name;
+    uintptr_t address_begin = std::numeric_limits<uintptr_t>::max();
+    uintptr_t address_end = 0;
+
+    bool contains(uintptr_t ip) const {
+        return ip == 0 || (address_begin <= ip && ip < address_end);
+    }
+};
+
+using PHDRCache = std::vector<PHDRCacheEntry>;
 std::atomic<PHDRCache*> phdr_cache {};
+// This flag is flipped inside the stack-trace signal handler. Force a static 
TLS access model so
+// reading it from our dl_iterate_phdr interposer does not call into the 
dynamic loader's TLS path.
+__thread bool use_phdr_cache __attribute__((tls_model("initial-exec"))) = 
false;
 
-} // namespace
+uintptr_t saturated_segment_end(uintptr_t begin, uintptr_t size) {
+    const uintptr_t max_address = std::numeric_limits<uintptr_t>::max();
+    return begin > max_address - size ? max_address : begin + size;
+}
 
-// Jemalloc heap profile follows libgcc's way of backtracing by default.
-// rewrites dl_iterate_phdr will cause Jemalloc to fail to run after enable 
profile.
+void copy_object_name(const char* source, RawPHDRCacheEntry* entry,
+                      RawPHDRCacheSnapshot* snapshot) {
+    if (source == nullptr) {
+        entry->name[0] = '\0';
+        return;
+    }
 
-// TODO, two solutions:
-// 1. Jemalloc specifies GNU libunwind as the prof backtracing way, but my 
test failed,
-//    `--enable-prof-libunwind` not work: 
https://github.com/jemalloc/jemalloc/issues/2504
-// 2. ClickHouse/libunwind solves Jemalloc profile backtracing, but the branch 
of ClickHouse/libunwind
-//    has been out of touch with GNU libunwind and LLVM libunwind, which will 
leave the fate to others.
-/*
-extern "C"
-#ifndef __clang__
-        [[gnu::visibility("default")]] [[gnu::externally_visible]]
-#endif
-        int
-        dl_iterate_phdr(int (*callback)(dl_phdr_info* info, size_t size, void* 
data), void* data) {
-    auto* current_phdr_cache = phdr_cache.load();
-    if (!current_phdr_cache) {
-        // Cache is not yet populated, pass through to the original function.
-        return getOriginalDLIteratePHDR()(callback, data);
+    size_t length = 0;
+    while (length + 1 < entry->name.size() && source[length] != '\0') {
+        entry->name[length] = source[length];
+        ++length;
+    }
+    entry->name[length] = '\0';
+    if (source[length] != '\0') {
+        snapshot->name_truncated = true;
+    }
+}
+
+int collectPHDRCacheEntry(dl_phdr_info* info, size_t /*size*/, void* data) {
+    auto* snapshot = reinterpret_cast<RawPHDRCacheSnapshot*>(data);
+    if (snapshot->size >= snapshot->entries.size()) {
+        snapshot->overflow = true;
+        return 0;
+    }
+
+    auto& entry = snapshot->entries[snapshot->size++];
+    entry.info = *info;
+    copy_object_name(info->dlpi_name, &entry, snapshot);
+
+    const size_t phdr_count = std::min<size_t>(info->dlpi_phnum, 
entry.phdrs.size());
+    if (phdr_count < info->dlpi_phnum) {
+        snapshot->phdr_truncated = true;
+    }
+    entry.phdr_count = phdr_count;
+    entry.info.dlpi_phnum = static_cast<ElfW(Half)>(phdr_count);
+    entry.info.dlpi_name = nullptr;
+    entry.info.dlpi_phdr = nullptr;
+
+    if (info->dlpi_phdr == nullptr) {
+        return 0;
+    }
+    std::memcpy(entry.phdrs.data(), info->dlpi_phdr, phdr_count * 
sizeof(ElfW(Phdr)));
+
+    for (size_t i = 0; i < phdr_count; ++i) {
+        const auto& phdr = entry.phdrs[i];
+        if (phdr.p_type != PT_LOAD || phdr.p_memsz == 0) {
+            continue;
+        }
+        const auto begin = static_cast<uintptr_t>(info->dlpi_addr + 
phdr.p_vaddr);
+        const auto end = saturated_segment_end(begin, 
static_cast<uintptr_t>(phdr.p_memsz));
+        entry.address_begin = std::min(entry.address_begin, begin);
+        entry.address_end = std::max(entry.address_end, end);
+    }
+    return 0;
+}
+
+PHDRCache* buildPHDRCache(const RawPHDRCacheSnapshot& snapshot) {
+    auto* cache = new PHDRCache;
+    cache->reserve(snapshot.size);
+    for (size_t i = 0; i < snapshot.size; ++i) {
+        const auto& raw_entry = snapshot.entries[i];
+        PHDRCacheEntry entry;
+        entry.info = raw_entry.info;
+        entry.phdrs.assign(raw_entry.phdrs.begin(), raw_entry.phdrs.begin() + 
raw_entry.phdr_count);
+        entry.name = raw_entry.name.data();
+        entry.address_begin = raw_entry.address_begin;
+        entry.address_end = raw_entry.address_end;
+        cache->emplace_back(std::move(entry));
+    }
+    for (auto& entry : *cache) {
+        entry.info.dlpi_phdr = entry.phdrs.data();
+        entry.info.dlpi_name = entry.name.c_str();
+    }
+    return cache;
+}
+
+int iteratePHDRCache(int (*callback)(dl_phdr_info* info, size_t size, void* 
data), void* data,
+                     uintptr_t ip) {
+    auto* current_phdr_cache = phdr_cache.load(std::memory_order_acquire);
+    if (current_phdr_cache == nullptr) {
+        return 0;
     }
 
     int result = 0;
     for (auto& entry : *current_phdr_cache) {
-        result = callback(&entry, offsetof(dl_phdr_info, dlpi_adds), data);
+        if (!entry.contains(ip)) {
+            continue;
+        }
+        result = callback(&entry.info, sizeof(dl_phdr_info), data);
         if (result != 0) {
             break;
         }
     }
     return result;
 }
-*/
+
+} // namespace
+
+extern "C"
+#ifndef __clang__
+        [[gnu::visibility("default")]] [[gnu::externally_visible]]
+#endif
+        int
+        dl_iterate_phdr(int (*callback)(dl_phdr_info* info, size_t size, void* 
data), void* data) {
+    if (!use_phdr_cache) {
+        return getOriginalDLIteratePHDR()(callback, data);
+    }
+
+    return iteratePHDRCache(callback, data, 0);
+}
+
+extern "C"
+#ifndef __clang__
+        [[gnu::visibility("default")]] [[gnu::externally_visible]]
+#endif
+        int
+        doris_unwind_iterate_phdr(int (*callback)(dl_phdr_info* info, size_t 
size, void* data),
+                                  void* data, uintptr_t ip) {
+    return iteratePHDRCache(callback, data, ip);
+}
 
 #include "util/debug/leak_annotations.h"
 
 void updatePHDRCache() {
     // Fill out ELF header cache for access without locking.
-    // This assumes no dynamic object loading/unloading after this point
+    // Old snapshots are intentionally kept alive because another thread may 
already be unwinding
+    // through the previous cache when a Doris-controlled dlopen/dlclose 
refreshes this one.
 
-    PHDRCache* new_phdr_cache = new PHDRCache;
+    auto raw_snapshot = std::make_unique<RawPHDRCacheSnapshot>();
     getOriginalDLIteratePHDR()(
-            [](dl_phdr_info* info, size_t /*size*/, void* data) {
+            [](dl_phdr_info* info, size_t size, void* data) {
                 // `info` is created by dl_iterate_phdr, which is a 
non-instrumented
                 // libc function, so we have to unpoison it manually.
                 __msan_unpoison(info, sizeof(*info));
 
-                reinterpret_cast<PHDRCache*>(data)->push_back(*info);
-                return 0;
+                return collectPHDRCacheEntry(info, size, data);
             },
-            new_phdr_cache);
-    phdr_cache.store(new_phdr_cache);
+            raw_snapshot.get());
+
+    PHDRCache* new_phdr_cache = buildPHDRCache(*raw_snapshot);
+    phdr_cache.store(new_phdr_cache, std::memory_order_release);
 
     /// Memory is intentionally leaked.
     ANNOTATE_LEAKING_OBJECT_PTR(new_phdr_cache);
 }
 
 bool hasPHDRCache() {
-    return phdr_cache.load() != nullptr;
+    return phdr_cache.load(std::memory_order_acquire) != nullptr;
+}
+
+void configureLibunwindPHDRCache() {
+#if defined(USE_UNWIND) && USE_UNWIND
+    static std::once_flag once;
+    std::call_once(once, [] {
+        unw_context_t context;
+        unw_cursor_t cursor;
+        (void)unw_getcontext(&context);
+        (void)unw_init_local(&cursor, &context);
+        // Doris-patched libunwind gets FDEs from the PHDR snapshot. Disable 
the global DWARF
+        // register-state cache so a signal handler cannot self-deadlock on 
libunwind's cache mutex
+        // after interrupting a thread that was already unwinding.
+        (void)unw_set_caching_policy(unw_local_addr_space, UNW_CACHE_NONE);
+    });
+#endif
+}
+
+ScopedPHDRCacheRead::ScopedPHDRCacheRead() : _previous(use_phdr_cache) {
+    use_phdr_cache = true;
+}
+
+ScopedPHDRCacheRead::~ScopedPHDRCacheRead() {
+    use_phdr_cache = _previous;
 }
 
 #else
 
 void updatePHDRCache() {}
 
+void configureLibunwindPHDRCache() {}
+
 #if defined(USE_MUSL)
 /// With statically linked with musl, dl_iterate_phdr is immutable.
 bool hasPHDRCache() {
@@ -148,4 +321,8 @@ bool hasPHDRCache() {
 }
 #endif
 
+ScopedPHDRCacheRead::ScopedPHDRCacheRead() = default;
+
+ScopedPHDRCacheRead::~ScopedPHDRCacheRead() = default;
+
 #endif
diff --git a/be/src/common/phdr_cache.h b/be/src/common/phdr_cache.h
index 30a212c94c0..da18b698f0d 100644
--- a/be/src/common/phdr_cache.h
+++ b/be/src/common/phdr_cache.h
@@ -20,21 +20,60 @@
 
 #pragma once
 
+#if defined(__linux__) && !defined(THREAD_SANITIZER) && !defined(USE_MUSL)
+#include <link.h>
+
+#include <cstddef>
+#include <cstdint>
+#endif
+
 /// This code was based on the code by Fedor Korotkiy 
https://www.linkedin.com/in/fedor-korotkiy-659a1838/
 
-/** Collects all dl_phdr_info items and caches them in a static array.
-  * Also rewrites dl_iterate_phdr with a lock-free version which consults the 
above cache
-  * thus eliminating scalability bottleneck in C++ exception unwinding.
-  * As a drawback, this only works if no dynamic object unloading happens 
after this point.
-  * This function is thread-safe. You should call it to update cache after 
loading new shared libraries.
-  * Otherwise exception handling from dlopened libraries won't work (will call 
std::terminate immediately).
-  * NOTE: dlopen is forbidden in our code.
+/** Collects loaded-object PHDR metadata into a lock-free snapshot.
+  * Doris uses this snapshot in two narrow places where entering glibc's 
loader lock can deadlock:
+  * the BE stack-trace signal handler and Doris-patched GNU libunwind's FDE 
lookup. Both paths may
+  * run while another thread is inside dlopen/dlclose or jemalloc profiling.
+  *
+  * Normal code paths must keep using the original glibc dl_iterate_phdr. 
Process-wide use of a
+  * snapshot can hide libraries loaded after the last refresh from sanitizers, 
JVM/Jindo native
+  * code, and C++ exception handling. Use ScopedPHDRCacheRead only around the 
minimal
+  * signal-handler unwind section; GNU libunwind reaches this cache through
+  * doris_unwind_iterate_phdr without changing ordinary dl_iterate_phdr 
callers.
+  *
+  * Old cache snapshots are intentionally leaked and remain readable by 
concurrent signal-handler
+  * unwinders.
   *
   * NOTE: It is disabled with Thread Sanitizer because TSan can only use 
original "dl_iterate_phdr" function.
   */
 void updatePHDRCache();
 
-/** Check if "dl_iterate_phdr" will be lock-free
-  * to determine if some features like Query Profiler can be used.
-  */
+/** Configure GNU libunwind to use the PHDR snapshot without its global 
register-state cache. */
+void configureLibunwindPHDRCache();
+
+/** Check if a PHDR snapshot is available for ScopedPHDRCacheRead. */
 bool hasPHDRCache();
+
+#if defined(__linux__) && !defined(THREAD_SANITIZER) && !defined(USE_MUSL)
+/**
+ * GNU libunwind calls this weak hook from Doris-patched thirdparty libunwind 
instead of calling
+ * glibc dl_iterate_phdr directly. The hook is intentionally narrower than the 
process-wide
+ * dl_iterate_phdr interposer: ordinary loader users still see glibc's live 
loader list, while
+ * libunwind reads Doris' lock-free PHDR snapshot to avoid loader-lock 
inversions during profiling
+ * and signal-context stack capture.
+ */
+extern "C" int doris_unwind_iterate_phdr(int (*callback)(dl_phdr_info* info, 
size_t size,
+                                                         void* data),
+                                         void* data, uintptr_t ip);
+#endif
+
+class ScopedPHDRCacheRead {
+public:
+    ScopedPHDRCacheRead();
+    ~ScopedPHDRCacheRead();
+
+    ScopedPHDRCacheRead(const ScopedPHDRCacheRead&) = delete;
+    ScopedPHDRCacheRead& operator=(const ScopedPHDRCacheRead&) = delete;
+
+private:
+    bool _previous = false;
+};
diff --git a/be/src/common/stack_trace.cpp b/be/src/common/stack_trace.cpp
index 4adcd108af6..9ff2b3e7931 100644
--- a/be/src/common/stack_trace.cpp
+++ b/be/src/common/stack_trace.cpp
@@ -22,6 +22,12 @@
 
 #include <fmt/format.h>
 
+#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
+#include <libunwind.h>
+#else
+#include <execinfo.h>
+#endif
+
 #include <atomic>
 #include <filesystem>
 #include <limits>
@@ -40,12 +46,6 @@
 #include "exec/common/hex.h"
 #include "util/string_util.h"
 
-#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
-#include <libunwind.h>
-#else
-#include <execinfo.h>
-#endif
-
 namespace {
 /// Currently this variable is set up once on server startup.
 /// But we use atomic just in case, so it is possible to be modified at 
runtime.
@@ -304,9 +304,7 @@ StackTrace::StackTrace(const ucontext_t& signal_context) {
 }
 
 void StackTrace::tryCapture() {
-    // When unw_backtrace is not available, fall back on the standard
-    // `backtrace` function from execinfo.h.
-#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__) // TODO
+#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
     size = unw_backtrace(frame_pointers.data(), capacity);
 #else
     size = backtrace(frame_pointers.data(), capacity);
diff --git a/be/src/common/stack_trace.h b/be/src/common/stack_trace.h
index 1d29b935f24..a075b11b893 100644
--- a/be/src/common/stack_trace.h
+++ b/be/src/common/stack_trace.h
@@ -39,8 +39,9 @@
 
 struct NoCapture {};
 
-/// Tries to capture current stack trace using libunwind or signal context
-/// NOTE: StackTrace calculation is signal safe only if updatePHDRCache() was 
called beforehand.
+/// Tries to capture current stack trace using libunwind or signal context.
+/// NOTE: Signal-context unwinding must enable ScopedPHDRCacheRead around the 
libunwind call so an
+/// interrupted thread does not re-enter glibc's loader lock through 
dl_iterate_phdr.
 class StackTrace {
 public:
     struct Frame {
diff --git a/be/src/common/symbol_index.cpp b/be/src/common/symbol_index.cpp
index 4fccbd0e696..3c144c3611d 100644
--- a/be/src/common/symbol_index.cpp
+++ b/be/src/common/symbol_index.cpp
@@ -26,10 +26,14 @@
 #include <pdqsort.h>
 
 #include <algorithm>
+#include <array>
 #include <cassert>
+#include <cstring>
 #include <filesystem>
+#include <mutex>
 #include <optional>
 
+#include "common/logging.h"
 #include "common/stack_trace.h"
 #include "exec/common/hex.h"
 
@@ -81,9 +85,11 @@ Otherwise you will get only exported symbols from program 
headers.
 #pragma clang diagnostic ignored "-Wunused-macros"
 #endif
 
+#define __msan_unpoison(X, Y)     // NOLINT
 #define __msan_unpoison_string(X) // NOLINT
 #if defined(__clang__) && defined(__has_feature)
 #if __has_feature(memory_sanitizer)
+#undef __msan_unpoison
 #undef __msan_unpoison_string
 #include <sanitizer/msan_interface.h>
 #endif
@@ -93,6 +99,35 @@ namespace doris {
 
 namespace {
 
+constexpr size_t MAX_SYMBOL_INDEX_LOADED_OBJECTS = 4096;
+constexpr size_t MAX_SYMBOL_INDEX_OBJECT_NAME = 4096;
+constexpr size_t MAX_SYMBOL_INDEX_BUILD_ID = 128;
+
+std::mutex& symbolIndexReloadMutex() {
+    static std::mutex lock;
+    return lock;
+}
+
+struct LoadedObject {
+    ElfW(Addr) base_address = 0;
+    std::array<char, MAX_SYMBOL_INDEX_OBJECT_NAME> name {};
+    size_t name_size = 0;
+    bool name_truncated = false;
+    std::array<char, MAX_SYMBOL_INDEX_BUILD_ID> build_id {};
+    size_t build_id_size = 0;
+    bool build_id_truncated = false;
+
+    std::string nameString() const { return {name.data(), name_size}; }
+    std::string buildIDString() const { return {build_id.data(), 
build_id_size}; }
+};
+
+struct LoadedObjectsSnapshot {
+    std::vector<LoadedObject> objects;
+    bool overflow = false;
+    bool name_truncated = false;
+    bool build_id_truncated = false;
+};
+
 /// Notes: "PHDR" is "Program Headers".
 /// To look at program headers, run:
 ///  readelf -l ./clickhouse-server
@@ -103,6 +138,73 @@ namespace {
 /// http://www.linker-aliens.org/blogs/ali/entry/inside_elf_symbol_tables/
 /// 
https://stackoverflow.com/questions/32088140/multiple-string-tables-in-elf-object
 
+template <size_t N>
+bool copyCString(const char* src, std::array<char, N>& dst, size_t& dst_size) {
+    dst_size = 0;
+    if (src == nullptr) {
+        dst[0] = '\0';
+        return false;
+    }
+
+    while (dst_size + 1 < N && src[dst_size] != '\0') {
+        dst[dst_size] = src[dst_size];
+        ++dst_size;
+    }
+    const bool truncated = src[dst_size] != '\0';
+    dst[dst_size] = '\0';
+    return truncated;
+}
+
+const char* alignELFNote(const char* ptr) {
+    const auto value = reinterpret_cast<uintptr_t>(ptr);
+    return reinterpret_cast<const char*>((value + 3) & ~uintptr_t {3});
+}
+
+bool copyBuildIDFromNotes(const char* note_begin, size_t size, LoadedObject& 
object) {
+    const char* pos = note_begin;
+    const char* end = note_begin + size;
+
+    while (pos + sizeof(ElfNhdr) <= end) {
+        ElfNhdr nhdr;
+        memcpy(&nhdr, pos, sizeof(nhdr));
+
+        const char* name_begin = pos + sizeof(ElfNhdr);
+        if (name_begin > end || static_cast<size_t>(end - name_begin) < 
nhdr.n_namesz) {
+            return false;
+        }
+
+        const char* desc_begin = alignELFNote(name_begin + nhdr.n_namesz);
+        if (desc_begin > end || static_cast<size_t>(end - desc_begin) < 
nhdr.n_descsz) {
+            return false;
+        }
+
+        if (nhdr.n_type == NT_GNU_BUILD_ID) {
+            const size_t copied = std::min<size_t>(nhdr.n_descsz, 
object.build_id.size());
+            memcpy(object.build_id.data(), desc_begin, copied);
+            object.build_id_size = copied;
+            object.build_id_truncated = nhdr.n_descsz > object.build_id.size();
+            return true;
+        }
+
+        pos = alignELFNote(desc_begin + nhdr.n_descsz);
+    }
+    return false;
+}
+
+void copyBuildIDFromProgramHeaders(dl_phdr_info* info, LoadedObject& object) {
+    for (size_t header_index = 0; header_index < info->dlpi_phnum; 
++header_index) {
+        const ElfPhdr& phdr = info->dlpi_phdr[header_index];
+        if (phdr.p_type != PT_NOTE) {
+            continue;
+        }
+
+        if (copyBuildIDFromNotes(reinterpret_cast<const char*>(info->dlpi_addr 
+ phdr.p_vaddr),
+                                 phdr.p_memsz, object)) {
+            return;
+        }
+    }
+}
+
 void updateResources(ElfW(Addr) base_address, std::string_view object_name, 
std::string_view name,
                      const void* address, SymbolIndex::Resources& resources) {
     const char* char_address = static_cast<const char*>(address);
@@ -139,8 +241,9 @@ void updateResources(ElfW(Addr) base_address, 
std::string_view object_name, std:
 /// 
https://stackoverflow.com/questions/15779185/list-all-the-functions-symbols-on-the-fly-in-c-code-on-a-linux-architecture
 /// It does not extract all the symbols (but only public - exported and used 
for dynamic linking),
 /// but will work if we cannot find or parse ELF files.
-void collectSymbolsFromProgramHeaders(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>& symbols,
-                                      SymbolIndex::Resources& resources) {
+[[maybe_unused]] void collectSymbolsFromProgramHeaders(dl_phdr_info* info,
+                                                       
std::vector<SymbolIndex::Symbol>& symbols,
+                                                       SymbolIndex::Resources& 
resources) {
     /* Iterate over all headers of the current shared lib
      * (first call is for the executable itself)
      */
@@ -267,7 +370,7 @@ void collectSymbolsFromProgramHeaders(dl_phdr_info* info, 
std::vector<SymbolInde
 }
 
 #if !defined USE_MUSL
-std::string getBuildIDFromProgramHeaders(dl_phdr_info* info) {
+[[maybe_unused]] std::string getBuildIDFromProgramHeaders(dl_phdr_info* info) {
     for (size_t header_index = 0; header_index < info->dlpi_phnum; 
++header_index) {
         const ElfPhdr& phdr = info->dlpi_phdr[header_index];
         if (phdr.p_type != PT_NOTE) {
@@ -281,8 +384,8 @@ std::string getBuildIDFromProgramHeaders(dl_phdr_info* 
info) {
 }
 #endif
 
-void collectSymbolsFromELFSymbolTable(dl_phdr_info* info, const Elf& elf,
-                                      const Elf::Section& symbol_table,
+void collectSymbolsFromELFSymbolTable(ElfW(Addr) base_address, 
std::string_view object_name,
+                                      const Elf& elf, const Elf::Section& 
symbol_table,
                                       const Elf::Section& string_table,
                                       std::vector<SymbolIndex::Symbol>& 
symbols,
                                       SymbolIndex::Resources& resources) {
@@ -307,21 +410,21 @@ void collectSymbolsFromELFSymbolTable(dl_phdr_info* info, 
const Elf& elf,
 
         SymbolIndex::Symbol symbol;
         symbol.address_begin =
-                reinterpret_cast<const void*>(info->dlpi_addr + 
symbol_table_entry->st_value);
+                reinterpret_cast<const void*>(base_address + 
symbol_table_entry->st_value);
         symbol.address_end = reinterpret_cast<const void*>(
-                info->dlpi_addr + symbol_table_entry->st_value + 
symbol_table_entry->st_size);
+                base_address + symbol_table_entry->st_value + 
symbol_table_entry->st_size);
         symbol.name = symbol_name;
 
         if (symbol_table_entry->st_size) {
             symbols.push_back(symbol);
         }
 
-        updateResources(info->dlpi_addr, info->dlpi_name, symbol.name, 
symbol.address_begin,
-                        resources);
+        updateResources(base_address, object_name, symbol.name, 
symbol.address_begin, resources);
     }
 }
 
-bool searchAndCollectSymbolsFromELFSymbolTable(dl_phdr_info* info, const Elf& 
elf,
+bool searchAndCollectSymbolsFromELFSymbolTable(ElfW(Addr) base_address,
+                                               std::string_view object_name, 
const Elf& elf,
                                                unsigned section_header_type,
                                                const char* string_table_name,
                                                
std::vector<SymbolIndex::Symbol>& symbols,
@@ -342,37 +445,35 @@ bool 
searchAndCollectSymbolsFromELFSymbolTable(dl_phdr_info* info, const Elf& el
         return false;
     }
 
-    collectSymbolsFromELFSymbolTable(info, elf, *symbol_table, *string_table, 
symbols, resources);
+    collectSymbolsFromELFSymbolTable(base_address, object_name, elf, 
*symbol_table, *string_table,
+                                     symbols, resources);
     return true;
 }
 
-void collectSymbolsFromELF(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>& symbols,
+void collectSymbolsFromELF(const LoadedObject& loaded_object,
+                           std::vector<SymbolIndex::Symbol>& symbols,
                            std::vector<SymbolIndex::Object>& objects,
                            SymbolIndex::Resources& resources, std::string& 
build_id) {
     std::string object_name;
-    std::string our_build_id;
+    std::string object_build_id = loaded_object.buildIDString();
 #if defined(USE_MUSL)
     object_name = "/proc/self/exe";
-    our_build_id = Elf(object_name).getBuildID();
-    build_id = our_build_id;
+    object_build_id = Elf(object_name).getBuildID();
+    build_id = object_build_id;
 #else
-    /// MSan does not know that the program segments in memory are initialized.
-    __msan_unpoison_string(info->dlpi_name);
-
-    object_name = info->dlpi_name;
-    our_build_id = getBuildIDFromProgramHeaders(info);
+    object_name = loaded_object.nameString();
 
     /// If the name is empty and there is a non-empty build-id - it's main 
executable.
     /// Find a elf file for the main executable and set the build-id.
     if (object_name.empty()) {
         object_name = "/proc/self/exe";
 
-        if (our_build_id.empty()) {
-            our_build_id = Elf(object_name).getBuildID();
+        if (object_build_id.empty()) {
+            object_build_id = Elf(object_name).getBuildID();
         }
 
         if (build_id.empty()) {
-            build_id = our_build_id;
+            build_id = object_build_id;
         }
     }
 #endif
@@ -405,14 +506,14 @@ void collectSymbolsFromELF(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>&
         object_name = local_debug_info_path;
     } else if (exists_not_empty(debug_info_path)) {
         object_name = debug_info_path;
-    } else if (build_id.size() >= 2) {
+    } else if (object_build_id.size() >= 2) {
         // Check if there is a .debug file in .build-id folder. For example:
         // 
/usr/lib/debug/.build-id/e4/0526a12e9a8f3819a18694f6b798f10c624d5c.debug
         std::string build_id_hex;
-        build_id_hex.resize(build_id.size() * 2);
+        build_id_hex.resize(object_build_id.size() * 2);
 
         char* pos = build_id_hex.data();
-        for (auto c : build_id) {
+        for (auto c : object_build_id) {
             write_hex_byte_lowercase(c, pos);
             pos += 2;
         }
@@ -435,7 +536,7 @@ void collectSymbolsFromELF(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>&
 
     std::string file_build_id = object.elf->getBuildID();
 
-    if (our_build_id != file_build_id) {
+    if (!object_build_id.empty() && object_build_id != file_build_id) {
         /// If debug info doesn't correspond to our binary, fallback to the 
info in our binary.
         if (object_name != canonical_path) {
             object_name = canonical_path;
@@ -443,7 +544,7 @@ void collectSymbolsFromELF(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>&
 
             /// But it can still be outdated, for example, if executable file 
was deleted from filesystem and replaced by another file.
             file_build_id = object.elf->getBuildID();
-            if (our_build_id != file_build_id) {
+            if (object_build_id != file_build_id) {
                 return;
             }
         } else {
@@ -451,32 +552,41 @@ void collectSymbolsFromELF(dl_phdr_info* info, 
std::vector<SymbolIndex::Symbol>&
         }
     }
 
-    object.address_begin = reinterpret_cast<const void*>(info->dlpi_addr);
-    object.address_end = reinterpret_cast<const void*>(info->dlpi_addr + 
object.elf->size());
+    object.address_begin = reinterpret_cast<const 
void*>(loaded_object.base_address);
+    object.address_end =
+            reinterpret_cast<const void*>(loaded_object.base_address + 
object.elf->size());
     object.name = object_name;
     objects.push_back(std::move(object));
-
-    searchAndCollectSymbolsFromELFSymbolTable(info, *objects.back().elf, 
SHT_SYMTAB, ".strtab",
-                                              symbols, resources);
-
-    /// Unneeded if they were parsed from "program headers" of loaded objects.
-#if defined USE_MUSL
-    searchAndCollectSymbolsFromELFSymbolTable(info, *objects.back().elf, 
SHT_DYNSYM, ".dynstr",
-                                              symbols, resources);
-#endif
+    const auto& indexed_object = objects.back();
+
+    searchAndCollectSymbolsFromELFSymbolTable(loaded_object.base_address, 
indexed_object.name,
+                                              *indexed_object.elf, SHT_SYMTAB, 
".strtab", symbols,
+                                              resources);
+    searchAndCollectSymbolsFromELFSymbolTable(loaded_object.base_address, 
indexed_object.name,
+                                              *indexed_object.elf, SHT_DYNSYM, 
".dynstr", symbols,
+                                              resources);
 }
 
 /* Callback for dl_iterate_phdr.
  * Is called by dl_iterate_phdr for every loaded shared lib until something
  * else than 0 is returned by one call of this function.
  */
-int collectSymbols(dl_phdr_info* info, size_t, void* data_ptr) {
-    SymbolIndex::Data& data = *reinterpret_cast<SymbolIndex::Data*>(data_ptr);
-
-    collectSymbolsFromProgramHeaders(info, data.symbols, data.resources);
-    collectSymbolsFromELF(info, data.symbols, data.objects, data.resources, 
data.build_id);
+int collectLoadedObject(dl_phdr_info* info, size_t, void* data_ptr) {
+    __msan_unpoison(info, sizeof(*info));
+    __msan_unpoison_string(info->dlpi_name);
+    auto& snapshot = *reinterpret_cast<LoadedObjectsSnapshot*>(data_ptr);
+    if (snapshot.objects.size() == snapshot.objects.capacity()) {
+        snapshot.overflow = true;
+        return 0;
+    }
 
-    /* Continue iterations */
+    LoadedObject object;
+    object.base_address = info->dlpi_addr;
+    object.name_truncated = copyCString(info->dlpi_name, object.name, 
object.name_size);
+    copyBuildIDFromProgramHeaders(info, object);
+    snapshot.name_truncated |= object.name_truncated;
+    snapshot.build_id_truncated |= object.build_id_truncated;
+    snapshot.objects.push_back(object);
     return 0;
 }
 
@@ -504,7 +614,34 @@ const T* find(const void* address, const std::vector<T>& 
vec) {
 } // namespace
 
 void SymbolIndex::update() {
-    dl_iterate_phdr(collectSymbols, &data);
+    LoadedObjectsSnapshot snapshot;
+    snapshot.objects.reserve(MAX_SYMBOL_INDEX_LOADED_OBJECTS);
+
+    // glibc holds the loader lock while running dl_iterate_phdr callbacks. 
The callback only copies
+    // fixed-size metadata into pre-reserved storage; ELF parsing and symbol 
vector growth happen
+    // after the loader lock is released so jemalloc profiling cannot re-enter 
libunwind from here.
+    dl_iterate_phdr(collectLoadedObject, &snapshot);
+
+    for (const auto& object : snapshot.objects) {
+        collectSymbolsFromELF(object, data.symbols, data.objects, 
data.resources, data.build_id);
+    }
+
+    if (snapshot.overflow) {
+        LOG(WARNING) << "SymbolIndex skipped loaded objects after "
+                     << MAX_SYMBOL_INDEX_LOADED_OBJECTS
+                     << " entries; stack symbolization may be incomplete";
+    }
+    if (snapshot.name_truncated) {
+        LOG(WARNING) << "SymbolIndex skipped at least one loaded object name 
longer than "
+                     << MAX_SYMBOL_INDEX_OBJECT_NAME - 1
+                     << " bytes; stack symbolization may be incomplete";
+    }
+    if (snapshot.build_id_truncated) {
+        LOG(WARNING) << "SymbolIndex truncated a loaded object build id longer 
than "
+                     << MAX_SYMBOL_INDEX_BUILD_ID
+                     << " bytes; stack symbolization may be incomplete";
+    }
+
     ::pdqsort(data.objects.begin(), data.objects.end(),
               [](const Object& a, const Object& b) { return a.address_begin < 
b.address_begin; });
     ::pdqsort(data.symbols.begin(), data.symbols.end(),
@@ -550,6 +687,7 @@ MultiVersion<SymbolIndex>::Version SymbolIndex::instance() {
 }
 
 void SymbolIndex::reload() {
+    std::lock_guard<std::mutex> lock(symbolIndexReloadMutex());
     instanceImpl().set(std::unique_ptr<SymbolIndex>(new SymbolIndex));
     /// Also drop stacktrace cache.
     StackTrace::dropCache();
diff --git a/be/src/service/doris_main.cpp b/be/src/service/doris_main.cpp
index 132a50c4403..12f5af0e2a4 100644
--- a/be/src/service/doris_main.cpp
+++ b/be/src/service/doris_main.cpp
@@ -48,7 +48,11 @@
 
 #include "cloud/cloud_backend_service.h"
 #include "cloud/config.h"
+#include "common/phdr_cache.h"
 #include "common/stack_trace.h"
+#if defined(__ELF__) && !defined(__FreeBSD__)
+#include "common/symbol_index.h"
+#endif
 #include "runtime/memory/mem_tracker_limiter.h"
 #include "storage/tablet/tablet_schema_cache.h"
 #include "storage/utils.h"
@@ -600,10 +604,18 @@ int main(int argc, char** argv) {
     LOG(INFO) << doris::DiskInfo::debug_string();
     LOG(INFO) << doris::MemInfo::debug_string();
 
-    // PHDR speed up exception handling, but exceptions from dynamically 
loaded libraries (dlopen)
-    // will work only after additional call of this function.
-    // rewrites dl_iterate_phdr will cause Jemalloc to fail to run after 
enable profile. see #
-    // updatePHDRCache();
+    // Doris-patched GNU libunwind reads PHDR metadata from our lock-free 
snapshot instead of
+    // entering glibc dl_iterate_phdr while jemalloc profiling or 
signal-context unwinding may
+    // already be involved in loader-lock-sensitive code. Configure libunwind 
before daemon threads
+    // start so all later heap-profile and stack-trace unwinds use the same 
lock-safe policy.
+    configureLibunwindPHDRCache();
+    updatePHDRCache();
+    LOG(INFO) << "PHDR cache enabled: " << hasPHDRCache();
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    auto symbol_index = doris::SymbolIndex::instance();
+    LOG(INFO) << "SymbolIndex preloaded: objects=" << 
symbol_index->objects().size()
+              << " symbols=" << symbol_index->symbols().size();
+#endif
     if (!doris::BackendOptions::init()) {
         exit(-1);
     }
diff --git a/be/src/service/http/action/be_thread_stack_action.cpp 
b/be/src/service/http/action/be_thread_stack_action.cpp
index 79241afd8b1..a7d89d743f3 100644
--- a/be/src/service/http/action/be_thread_stack_action.cpp
+++ b/be/src/service/http/action/be_thread_stack_action.cpp
@@ -56,6 +56,7 @@
 #endif
 
 #include "common/logging.h"
+#include "common/phdr_cache.h"
 #include "common/stack_trace.h"
 #include "service/http/http_channel.h"
 #include "service/http/http_headers.h"
@@ -73,7 +74,6 @@ constexpr std::string_view HEADER_TEXT = "text/plain; 
charset=utf-8";
 constexpr int STACK_TRACE_SIGNAL_OFFSET = 6;
 constexpr int DEFAULT_TIMEOUT_MS = 100;
 constexpr int MAX_TIMEOUT_MS = 10000;
-constexpr size_t MAX_MEMORY_RANGES = 8192;
 constexpr std::string_view DEFAULT_DWARF_MODE = "FAST";
 
 struct ThreadInfo {
@@ -81,22 +81,9 @@ struct ThreadInfo {
     std::string name;
 };
 
-struct MemoryRange {
-    uintptr_t begin = 0;
-    uintptr_t end = 0;
-};
-
-enum class FramePointerStatus {
-    END_OF_CHAIN,
-    NO_CONTEXT,
-    UNSUPPORTED_ARCH,
-    NO_STACK_RANGE,
-    INVALID_FRAME_POINTER,
-    FRAME_LIMIT,
-};
-
 enum class SignalContextUnwindStatus {
     NOT_ATTEMPTED,
+    NO_CONTEXT,
     END_OF_STACK,
     INIT_ERROR,
     GET_IP_ERROR,
@@ -105,16 +92,11 @@ enum class SignalContextUnwindStatus {
     UNSUPPORTED,
 };
 
-struct FramePointerCapture {
+struct SignalContextCapture {
     StackTrace::FramePointers frame_pointers {};
     size_t size = 0;
-    uintptr_t stack_begin = 0;
-    uintptr_t stack_end = 0;
-    FramePointerStatus fp_status = FramePointerStatus::NO_CONTEXT;
-    bool used_signal_context_unwind = false;
-    SignalContextUnwindStatus signal_context_unwind_status =
-            SignalContextUnwindStatus::NOT_ATTEMPTED;
-    int signal_context_unwind_error = 0;
+    SignalContextUnwindStatus unwind_status = 
SignalContextUnwindStatus::NOT_ATTEMPTED;
+    int unwind_error = 0;
 };
 
 struct ThreadSyscall {
@@ -128,16 +110,11 @@ std::atomic<pid_t> g_server_pid {0};
 std::atomic<int> g_sequence {0};
 // The signal handler cannot allocate per-request state safely, so it 
publishes into one
 // process-wide slot. The latch protects that slot from nested or back-to-back 
signals while the
-// coordinator is still copying or unwinding the previous thread's context.
+// HTTP worker is still copying the previous thread's captured PCs.
 std::atomic<int> g_active_sequence {0};
 std::atomic<int> g_data_ready_sequence {0};
-std::atomic<int> g_unwind_wait_sequence {0};
-std::atomic<int> g_unwind_release_sequence {0};
 std::atomic<bool> g_signal_latch {false};
-FramePointerCapture g_signal_capture;
-ucontext_t g_signal_context {};
-std::array<MemoryRange, MAX_MEMORY_RANGES> g_memory_ranges {};
-size_t g_memory_range_count = 0;
+SignalContextCapture g_signal_capture;
 int g_notification_pipe[2] = {-1, -1};
 
 int rt_tgsigqueueinfo(pid_t tgid, pid_t tid, int sig, siginfo_t* info) {
@@ -156,135 +133,33 @@ pid_t get_current_tid() {
     return static_cast<pid_t>(syscall(SYS_gettid));
 }
 
-bool read_signal_registers(const ucontext_t* context, uintptr_t* pc, 
uintptr_t* fp, uintptr_t* sp) {
-    if (context == nullptr) {
-        return false;
-    }
-
-#if defined(__x86_64__)
-    *pc = static_cast<uintptr_t>(context->uc_mcontext.gregs[REG_RIP]);
-    *fp = static_cast<uintptr_t>(context->uc_mcontext.gregs[REG_RBP]);
-    *sp = static_cast<uintptr_t>(context->uc_mcontext.gregs[REG_RSP]);
-    return true;
-#elif defined(__aarch64__)
-    *pc = static_cast<uintptr_t>(context->uc_mcontext.pc);
-    *fp = static_cast<uintptr_t>(context->uc_mcontext.regs[29]);
-    *sp = static_cast<uintptr_t>(context->uc_mcontext.sp);
-    return true;
-#else
-    return false;
-#endif
-}
-
-bool range_contains(const MemoryRange& range, uintptr_t address) {
-    return address >= range.begin && address < range.end;
-}
-
-bool frame_record_is_readable(const MemoryRange& range, uintptr_t fp) {
-    constexpr uintptr_t frame_record_size = sizeof(uintptr_t) * 2;
-    // The interrupted register can come from code without frame pointers or 
from a prologue/
-    // epilogue. Bounds plus alignment keep the handler from reinterpreting 
arbitrary stack bytes
-    // as a frame record.
-    return fp % alignof(uintptr_t) == 0 && fp >= range.begin &&
-           fp <= std::numeric_limits<uintptr_t>::max() - frame_record_size &&
-           fp + frame_record_size <= range.end;
-}
-
-const MemoryRange* find_stack_range(uintptr_t sp, uintptr_t fp) {
-    for (size_t i = 0; i < g_memory_range_count; ++i) {
-        const auto& range = g_memory_ranges[i];
-        if (range_contains(range, sp) && range_contains(range, fp)) {
-            return &range;
-        }
-    }
-    return nullptr;
-}
-
-void append_frame(FramePointerCapture* capture, uintptr_t pc) {
+void append_frame(SignalContextCapture* capture, uintptr_t pc) {
     if (pc == 0 || capture->size >= capture->frame_pointers.size()) {
         return;
     }
     capture->frame_pointers[capture->size++] = reinterpret_cast<void*>(pc);
 }
 
-void capture_frame_pointers_from_context(const ucontext_t* context, 
FramePointerCapture* capture) {
-    *capture = FramePointerCapture {};
-
-    uintptr_t pc = 0;
-    uintptr_t fp = 0;
-    uintptr_t sp = 0;
-    if (!read_signal_registers(context, &pc, &fp, &sp)) {
-#if defined(__x86_64__) || defined(__aarch64__)
-        capture->fp_status = FramePointerStatus::NO_CONTEXT;
-#else
-        capture->fp_status = FramePointerStatus::UNSUPPORTED_ARCH;
-#endif
-        return;
-    }
-
-    append_frame(capture, pc);
-    const MemoryRange* stack_range = find_stack_range(sp, fp);
-    if (stack_range == nullptr || fp < sp) {
-        capture->fp_status = FramePointerStatus::NO_STACK_RANGE;
-        return;
-    }
-    if (!frame_record_is_readable(*stack_range, fp)) {
-        capture->fp_status = FramePointerStatus::INVALID_FRAME_POINTER;
+void capture_signal_context_unwind(const ucontext_t* context, 
SignalContextCapture* capture) {
+    *capture = SignalContextCapture {};
+    if (context == nullptr) {
+        capture->unwind_status = SignalContextUnwindStatus::NO_CONTEXT;
         return;
     }
 
-    capture->stack_begin = stack_range->begin;
-    capture->stack_end = stack_range->end;
-
-    uintptr_t current_fp = fp;
-    while (capture->size < capture->frame_pointers.size()) {
-        if (!frame_record_is_readable(*stack_range, current_fp)) {
-            capture->fp_status = FramePointerStatus::INVALID_FRAME_POINTER;
-            return;
-        }
-
-        const auto* frame_record = reinterpret_cast<const 
uintptr_t*>(current_fp);
-        const uintptr_t next_fp = frame_record[0];
-        const uintptr_t return_address = frame_record[1];
-        append_frame(capture, return_address);
-
-        if (next_fp == 0) {
-            capture->fp_status = FramePointerStatus::END_OF_CHAIN;
-            return;
-        }
-        if (next_fp <= current_fp || !range_contains(*stack_range, next_fp) ||
-            !frame_record_is_readable(*stack_range, next_fp)) {
-            capture->fp_status = FramePointerStatus::INVALID_FRAME_POINTER;
-            return;
-        }
-        current_fp = next_fp;
-    }
-
-    capture->fp_status = FramePointerStatus::FRAME_LIMIT;
-}
-
-bool should_fallback_to_signal_context_unwind(const FramePointerCapture& 
capture) {
-    return capture.fp_status != FramePointerStatus::END_OF_CHAIN &&
-           capture.fp_status != FramePointerStatus::FRAME_LIMIT;
-}
-
-void capture_signal_context_unwind(const ucontext_t* context, 
FramePointerCapture* capture) {
 #if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
-    StackTrace::FramePointers frame_pointers {};
-    size_t size = 0;
-
     unw_cursor_t cursor;
     auto* unwind_context = 
reinterpret_cast<unw_context_t*>(const_cast<ucontext_t*>(context));
     int rc = unw_init_local2(&cursor, unwind_context, UNW_INIT_SIGNAL_FRAME);
     if (rc < 0) {
-        capture->signal_context_unwind_status = 
SignalContextUnwindStatus::INIT_ERROR;
-        capture->signal_context_unwind_error = rc;
+        capture->unwind_status = SignalContextUnwindStatus::INIT_ERROR;
+        capture->unwind_error = rc;
         return;
     }
 
     SignalContextUnwindStatus status = SignalContextUnwindStatus::END_OF_STACK;
     int unwind_error = 0;
-    while (size < frame_pointers.size()) {
+    while (capture->size < capture->frame_pointers.size()) {
         unw_word_t ip = 0;
         rc = unw_get_reg(&cursor, UNW_REG_IP, &ip);
         if (rc < 0) {
@@ -293,7 +168,7 @@ void capture_signal_context_unwind(const ucontext_t* 
context, FramePointerCaptur
             break;
         }
         if (ip != 0) {
-            frame_pointers[size++] = reinterpret_cast<void*>(ip);
+            append_frame(capture, static_cast<uintptr_t>(ip));
         }
 
         rc = unw_step(&cursor);
@@ -308,26 +183,22 @@ void capture_signal_context_unwind(const ucontext_t* 
context, FramePointerCaptur
         unwind_error = rc;
         break;
     }
-    if (size == frame_pointers.size()) {
+    if (capture->size == capture->frame_pointers.size()) {
         status = SignalContextUnwindStatus::FRAME_LIMIT;
     }
 
-    capture->signal_context_unwind_status = status;
-    capture->signal_context_unwind_error = unwind_error;
-    if (size > capture->size) {
-        capture->frame_pointers = frame_pointers;
-        capture->size = size;
-        capture->used_signal_context_unwind = true;
-    }
+    capture->unwind_status = status;
+    capture->unwind_error = unwind_error;
 #else
-    capture->signal_context_unwind_status = 
SignalContextUnwindStatus::UNSUPPORTED;
+    capture->unwind_status = SignalContextUnwindStatus::UNSUPPORTED;
 #endif
 }
 
-// SAFETY: this signal handler never symbolicates, never calls libunwind, and 
never calls
-// dl_iterate_phdr or malloc. It walks frame records inside the preloaded 
stack mapping and,
-// when frame-pointer walking is insufficient, copies the ucontext_t for the 
coordinator thread
-// to unwind while this thread remains paused in the handler.
+// SAFETY: this handler only runs libunwind against the kernel-provided signal 
context and writes
+// raw PCs into a preallocated process-wide slot. It never symbolicates, logs, 
allocates strings, or
+// opens /proc. The PHDR cache scope is deliberately limited to this handler: 
a target thread may be
+// interrupted while already holding glibc's loader lock, but normal 
sanitizer/JVM/exception paths
+// must still see the live loader list through the original dl_iterate_phdr 
implementation.
 void stack_trace_signal_handler(int /*sig*/, siginfo_t* info, void* context) {
     auto saved_errno = errno;
 
@@ -349,12 +220,9 @@ void stack_trace_signal_handler(int /*sig*/, siginfo_t* 
info, void* context) {
     }
 
     const auto* signal_context = reinterpret_cast<const ucontext_t*>(context);
-    capture_frame_pointers_from_context(signal_context, &g_signal_capture);
-    const bool needs_coordinator_unwind =
-            should_fallback_to_signal_context_unwind(g_signal_capture);
-    if (needs_coordinator_unwind) {
-        g_signal_context = *signal_context;
-        g_unwind_wait_sequence.store(notification_sequence, 
std::memory_order_release);
+    {
+        ScopedPHDRCacheRead phdr_cache_scope;
+        capture_signal_context_unwind(signal_context, &g_signal_capture);
     }
     g_data_ready_sequence.store(notification_sequence, 
std::memory_order_release);
 
@@ -364,19 +232,6 @@ void stack_trace_signal_handler(int /*sig*/, siginfo_t* 
info, void* context) {
         (void)res;
     }
 
-    while (needs_coordinator_unwind &&
-           g_unwind_release_sequence.load(std::memory_order_acquire) != 
notification_sequence &&
-           g_active_sequence.load(std::memory_order_acquire) == 
notification_sequence) {
-#if defined(__x86_64__)
-        __builtin_ia32_pause();
-#else
-        std::atomic_signal_fence(std::memory_order_seq_cst);
-#endif
-    }
-
-    if (needs_coordinator_unwind) {
-        g_unwind_wait_sequence.store(0, std::memory_order_release);
-    }
     g_signal_latch.store(false, std::memory_order_release);
     errno = saved_errno;
 }
@@ -386,6 +241,13 @@ void install_signal_handler() {
         LOG(FATAL) << "SIGRTMIN+" << STACK_TRACE_SIGNAL_OFFSET << " exceeds 
SIGRTMAX";
     }
 
+#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
+    updatePHDRCache();
+    if (!hasPHDRCache()) {
+        LOG(FATAL) << "BE thread stack trace requires lock-free PHDR cache";
+    }
+#endif
+
     g_server_pid.store(getpid(), std::memory_order_release);
     if (pipe2(g_notification_pipe, O_CLOEXEC | O_NONBLOCK) != 0) {
         PLOG(FATAL) << "failed to create stack trace notification pipe";
@@ -592,24 +454,6 @@ bool parse_hex_u64(std::string_view value, uint64_t* 
result) {
     return true;
 }
 
-bool parse_maps_range(std::string_view value, MemoryRange* range) {
-    const size_t dash = value.find('-');
-    if (dash == std::string_view::npos) {
-        return false;
-    }
-
-    uint64_t begin = 0;
-    uint64_t end = 0;
-    if (!parse_hex_u64(value.substr(0, dash), &begin) ||
-        !parse_hex_u64(value.substr(dash + 1), &end) || begin >= end) {
-        return false;
-    }
-
-    range->begin = static_cast<uintptr_t>(begin);
-    range->end = static_cast<uintptr_t>(end);
-    return true;
-}
-
 bool wait_for_signal_handler_idle(int timeout_ms) {
     const auto deadline = std::chrono::steady_clock::now() + 
std::chrono::milliseconds(timeout_ms);
     while (g_signal_latch.load(std::memory_order_acquire)) {
@@ -621,54 +465,34 @@ bool wait_for_signal_handler_idle(int timeout_ms) {
     return true;
 }
 
-bool release_signal_handler_and_wait(int sequence, int timeout_ms) {
-    // A published stack only means the data slot is ready. The target thread 
may still be inside
-    // the handler waiting for fallback unwind release; starting the next TID 
before the latch drops
-    // can make that next handler return without publishing anything.
-    g_unwind_release_sequence.store(sequence, std::memory_order_release);
+bool finish_signal_capture_and_wait(int timeout_ms) {
     g_active_sequence.store(0, std::memory_order_release);
     return wait_for_signal_handler_idle(timeout_ms);
 }
 
-bool load_readable_writable_mappings(int timeout_ms, std::string* error) {
-    // Stack bounds are read before any signal is sent because the handler 
must not open /proc,
-    // allocate, or take locks while the target thread is asynchronously 
interrupted.
+bool prepare_signal_capture(int timeout_ms, std::string* error) {
+    // Refresh before sending signals. This may take glibc's loader lock, so 
it must happen on the
+    // coordinator thread rather than in the interrupted target thread's 
signal handler.
     g_active_sequence.store(0, std::memory_order_release);
     if (!wait_for_signal_handler_idle(timeout_ms)) {
         *error = "previous stack trace signal handler is still running";
         return false;
     }
-    g_memory_range_count = 0;
 
-    std::ifstream maps("/proc/self/maps");
-    if (!maps.is_open()) {
-        *error = fmt::format("failed to open /proc/self/maps: {}", 
std::strerror(errno));
+#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
+    updatePHDRCache();
+    if (!hasPHDRCache()) {
+        *error = "lock-free PHDR cache is unavailable";
         return false;
     }
-
-    std::string line;
-    while (std::getline(maps, line)) {
-        std::istringstream iss(line);
-        std::string address_range;
-        std::string permissions;
-        if (!(iss >> address_range >> permissions)) {
-            continue;
-        }
-        if (permissions.size() < 2 || permissions[0] != 'r' || permissions[1] 
!= 'w') {
-            continue;
-        }
-
-        MemoryRange range;
-        if (!parse_maps_range(address_range, &range)) {
-            continue;
-        }
-        if (g_memory_range_count >= g_memory_ranges.size()) {
-            *error = fmt::format("too many readable writable mappings, 
max={}", MAX_MEMORY_RANGES);
-            g_memory_range_count = 0;
-            return false;
-        }
-        g_memory_ranges[g_memory_range_count++] = range;
-    }
+#elif !defined(USE_UNWIND) || !USE_UNWIND
+    *error = "BE thread stack trace requires libunwind; this build was 
compiled with "
+             "USE_UNWIND=OFF";
+    return false;
+#else
+    *error = "signal-context libunwind is unsupported on this architecture";
+    return false;
+#endif
 
     return true;
 }
@@ -864,28 +688,12 @@ std::optional<ThreadSyscall> 
current_interrupt_sensitive_syscall(pid_t tid) {
     return ThreadSyscall {.number = number, .name = syscall_name(number)};
 }
 
-std::string fp_status_to_string(FramePointerStatus status) {
-    switch (status) {
-    case FramePointerStatus::END_OF_CHAIN:
-        return "end_of_chain";
-    case FramePointerStatus::NO_CONTEXT:
-        return "no_context";
-    case FramePointerStatus::UNSUPPORTED_ARCH:
-        return "unsupported_arch";
-    case FramePointerStatus::NO_STACK_RANGE:
-        return "no_stack_range";
-    case FramePointerStatus::INVALID_FRAME_POINTER:
-        return "invalid_frame_pointer";
-    case FramePointerStatus::FRAME_LIMIT:
-        return "frame_limit";
-    }
-    return "unknown";
-}
-
 std::string signal_context_unwind_status_to_string(SignalContextUnwindStatus 
status) {
     switch (status) {
     case SignalContextUnwindStatus::NOT_ATTEMPTED:
         return "not_attempted";
+    case SignalContextUnwindStatus::NO_CONTEXT:
+        return "no_context";
     case SignalContextUnwindStatus::END_OF_STACK:
         return "end_of_stack";
     case SignalContextUnwindStatus::INIT_ERROR:
@@ -902,22 +710,15 @@ std::string 
signal_context_unwind_status_to_string(SignalContextUnwindStatus sta
     return "unknown";
 }
 
-std::string describe_frame_pointer_capture(const FramePointerCapture& capture) 
{
+std::string describe_signal_context_capture(const SignalContextCapture& 
capture) {
     std::stringstream out;
-    out << "capture_method="
-        << (capture.used_signal_context_unwind ? "signal_context_libunwind" : 
"frame_pointer");
+    out << "capture_method=signal_context_libunwind";
     out << " frames=" << capture.size;
-    out << " fp_status=" << fp_status_to_string(capture.fp_status);
-    if (capture.signal_context_unwind_status != 
SignalContextUnwindStatus::NOT_ATTEMPTED) {
-        out << " unwind_status="
-            << 
signal_context_unwind_status_to_string(capture.signal_context_unwind_status);
-        if (capture.signal_context_unwind_error != 0) {
-            out << " unwind_error=" << capture.signal_context_unwind_error;
-        }
-    }
-    if (capture.stack_begin != 0 || capture.stack_end != 0) {
-        out << fmt::format(" stack_bounds=0x{:x}-0x{:x}", capture.stack_begin, 
capture.stack_end);
+    out << " unwind_status=" << 
signal_context_unwind_status_to_string(capture.unwind_status);
+    if (capture.unwind_error != 0) {
+        out << " unwind_error=" << capture.unwind_error;
     }
+    out << " phdr_cache=" << (hasPHDRCache() ? "true" : "false");
     return out.str();
 }
 
@@ -972,7 +773,7 @@ bool wait_for_stack_trace(int sequence, int timeout_ms) {
     }
 }
 
-std::string symbolize_stack_trace(const FramePointerCapture& capture,
+std::string symbolize_stack_trace(const SignalContextCapture& capture,
                                   const std::string& dwarf_mode) {
     StackTrace::FramePointers frame_pointers = capture.frame_pointers;
     return StackTrace::toString(frame_pointers.data(), 0, capture.size, 
dwarf_mode);
@@ -1003,7 +804,8 @@ CaptureResult capture_thread_stack(pid_t tid, const 
std::string& dwarf_mode, int
                                    bool skip_blocking_syscalls) {
     if (tid == get_current_tid()) {
         return {CaptureStatus::CURRENT_THREAD, 
capture_current_thread_stack(dwarf_mode), "",
-                "capture_method=current_thread_stacktrace"};
+                fmt::format("capture_method=current_thread_libunwind 
phdr_cache={}",
+                            hasPHDRCache() ? "true" : "false")};
     }
 
     if (skip_blocking_syscalls) {
@@ -1017,16 +819,13 @@ CaptureResult capture_thread_stack(pid_t tid, const 
std::string& dwarf_mode, int
         return {CaptureStatus::SIGNAL_BLOCKED, "", "", ""};
     }
 
-    // The handler publishes through process-global state, not per-thread 
storage. Waiting here is
-    // the guardrail that keeps a previous slow-to-exit handler from causing 
this TID's signal to be
-    // dropped by the latch CAS.
+    // The handler publishes through process-global state, not per-thread 
storage. Waiting here
+    // keeps a still-running handler from causing this TID's signal to be 
dropped by the latch CAS.
     if (!wait_for_signal_handler_idle(timeout_ms)) {
         return {CaptureStatus::TIMEOUT, "", "", 
"previous_signal_handler_still_running"};
     }
 
     int sequence = g_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
-    g_unwind_release_sequence.store(0, std::memory_order_release);
-    g_unwind_wait_sequence.store(0, std::memory_order_release);
     g_active_sequence.store(sequence, std::memory_order_release);
     siginfo_t signal_info {};
     signal_info.si_code = SI_QUEUE;
@@ -1044,25 +843,26 @@ CaptureResult capture_thread_stack(pid_t tid, const 
std::string& dwarf_mode, int
     }
 
     if (!wait_for_stack_trace(sequence, timeout_ms)) {
-        const bool handler_idle = release_signal_handler_and_wait(sequence, 
timeout_ms);
+        const bool handler_idle = finish_signal_capture_and_wait(timeout_ms);
         return {CaptureStatus::TIMEOUT, "", "",
                 handler_idle ? "" : "signal_handler_release_timeout"};
     }
 
-    FramePointerCapture capture = g_signal_capture;
-    if (should_fallback_to_signal_context_unwind(capture) &&
-        g_unwind_wait_sequence.load(std::memory_order_acquire) == sequence) {
-        capture_signal_context_unwind(&g_signal_context, &capture);
-    }
-    if (!release_signal_handler_and_wait(sequence, timeout_ms)) {
-        // Returning the captured stack while the target is still in the 
handler would hide a much
-        // more serious diagnostic side effect. Treat it as a timeout so the 
summary reflects the
-        // release failure explicitly.
+    SignalContextCapture capture = g_signal_capture;
+    if (!finish_signal_capture_and_wait(timeout_ms)) {
+        // The handler no longer waits for coordinator unwinding; a non-idle 
latch now means the
+        // signal handler itself is stuck, which is a diagnostic side effect 
we must surface.
         return {CaptureStatus::TIMEOUT, "", "", 
"signal_handler_release_timeout"};
     }
+    if (capture.size == 0 || capture.unwind_status == 
SignalContextUnwindStatus::NO_CONTEXT ||
+        capture.unwind_status == SignalContextUnwindStatus::INIT_ERROR ||
+        capture.unwind_status == SignalContextUnwindStatus::GET_IP_ERROR ||
+        capture.unwind_status == SignalContextUnwindStatus::UNSUPPORTED) {
+        return {CaptureStatus::SIGNAL_ERROR, "", "", 
describe_signal_context_capture(capture)};
+    }
 
     return {CaptureStatus::OK, symbolize_stack_trace(capture, dwarf_mode), "",
-            describe_frame_pointer_capture(capture)};
+            describe_signal_context_capture(capture)};
 }
 
 std::string status_to_string(CaptureStatus status) {
@@ -1115,7 +915,11 @@ void append_thread_result(std::stringstream& out, const 
ThreadInfo& thread,
 void BeThreadStackAction::handle(HttpRequest* req) {
     req->add_output_header(HttpHeaders::CONTENT_TYPE, HEADER_TEXT.data());
 
-#ifndef __linux__
+#if !defined(USE_UNWIND) || !USE_UNWIND
+    HttpChannel::send_reply(req, HttpStatus::NOT_IMPLEMENTED,
+                            "BE thread stack trace requires libunwind; this 
build was compiled "
+                            "with USE_UNWIND=OFF.\n");
+#elif !defined(__linux__)
     HttpChannel::send_reply(req, HttpStatus::NOT_IMPLEMENTED,
                             "BE thread stack trace is only supported on 
Linux.\n");
 #else
@@ -1157,7 +961,7 @@ void BeThreadStackAction::handle(HttpRequest* req) {
     }
 
     auto threads = list_threads(tid_filter);
-    if (!load_readable_writable_mappings(timeout_ms, &error)) {
+    if (!prepare_signal_capture(timeout_ms, &error)) {
         HttpChannel::send_reply(req, HttpStatus::INTERNAL_SERVER_ERROR, error 
+ "\n");
         return;
     }
@@ -1170,8 +974,8 @@ void BeThreadStackAction::handle(HttpRequest* req) {
     out << "timeout_ms_per_thread: " << timeout_ms << '\n';
     out << "dwarf_location_info_mode: " << dwarf_mode << '\n';
     out << "skip_blocking_syscalls: " << (skip_blocking_syscalls ? "true" : 
"false") << '\n';
-    out << "signal_handler_unwinder: "
-           
"frame_pointer_with_coordinator_signal_context_libunwind_fallback\n\n";
+    out << "phdr_cache: " << (hasPHDRCache() ? "true" : "false") << '\n';
+    out << "signal_handler_unwinder: signal_context_libunwind\n\n";
 
     int captured = 0;
     int skipped = 0;
diff --git a/be/src/util/dynamic_util.cpp b/be/src/util/dynamic_util.cpp
index 1f09c11b6d7..6f5f56f5dfe 100644
--- a/be/src/util/dynamic_util.cpp
+++ b/be/src/util/dynamic_util.cpp
@@ -22,6 +22,11 @@
 
 #include <dlfcn.h>
 
+#include "common/phdr_cache.h"
+#if defined(__ELF__) && !defined(__FreeBSD__)
+#include "common/symbol_index.h"
+#endif
+
 namespace doris {
 
 Status dynamic_lookup(void* handle, const char* symbol, void** fn_ptr) {
@@ -44,6 +49,12 @@ Status dynamic_open(const char* library, void** handle) {
         return Status::InternalError("Unable to load {}\ndlerror: {}", 
library, dlerror());
     }
 
+    // Doris-controlled dynamic loads should be visible to diagnostic stack 
snapshots without
+    // forcing the next /api/stack_trace request to rebuild loader-derived 
state on demand.
+    updatePHDRCache();
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    SymbolIndex::reload();
+#endif
     return Status::OK();
 }
 
@@ -52,6 +63,12 @@ void dynamic_close(void* handle) {
 // https://github.com/google/sanitizers/issues/89
 #if !defined(ADDRESS_SANITIZER) && !defined(LEAK_SANITIZER)
     dlclose(handle);
+    // Refresh after dlclose so later symbolization does not keep stale 
Doris-controlled library
+    // entries. SymbolIndex::reload() serializes concurrent rebuilds 
internally.
+    updatePHDRCache();
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    SymbolIndex::reload();
+#endif
 #endif
 }
 
diff --git a/be/src/util/libjvm_loader.cpp b/be/src/util/libjvm_loader.cpp
index 02375f81422..5c085cc23b2 100644
--- a/be/src/util/libjvm_loader.cpp
+++ b/be/src/util/libjvm_loader.cpp
@@ -23,7 +23,11 @@
 #include <filesystem>
 #include <mutex>
 
+#include "common/phdr_cache.h"
 #include "common/status.h"
+#if defined(__ELF__) && !defined(__FreeBSD__)
+#include "common/symbol_index.h"
+#endif
 #include "jni.h"
 #include "jni_md.h"
 
@@ -52,6 +56,14 @@ doris::Status resolve_symbol(T& pointer, void* handle, const 
char* symbol) {
                    : doris::Status::RuntimeError("Failed to resolve the symbol 
{}", symbol);
 }
 
+void close_jvm_handle(void* handle) {
+    dlclose(handle);
+    ::updatePHDRCache();
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    doris::SymbolIndex::reload();
+#endif
+}
+
 } // namespace
 
 namespace doris {
@@ -88,11 +100,19 @@ Status LibJVMLoader::load() {
     static std::once_flag resolve_symbols;
     static Status status;
     std::call_once(resolve_symbols, [this]() {
-        _handle = std::unique_ptr<void, void 
(*)(void*)>(dlopen(_library.c_str(), RTLD_LAZY),
-                                                         [](void* handle) { 
dlclose(handle); });
-        if (!_handle) {
-            status = Status::RuntimeError(dlerror());
-            return;
+        {
+            void* handle = dlopen(_library.c_str(), RTLD_LAZY);
+            if (handle == nullptr) {
+                status = Status::RuntimeError(dlerror());
+                return;
+            }
+            _handle = std::unique_ptr<void, void (*)(void*)>(handle, 
close_jvm_handle);
+            // libjvm bypasses the generic dynamic_open wrapper, so refresh 
the same diagnostic
+            // snapshots here to avoid first-use loader/symbol rebuilds on the 
stack HTTP path.
+            ::updatePHDRCache();
+#if defined(__ELF__) && !defined(__FreeBSD__)
+            SymbolIndex::reload();
+#endif
         }
 
         if (status = resolve_symbol(JNI_GetCreatedJavaVMs, _handle.get(), 
"JNI_GetCreatedJavaVMs");
diff --git a/be/src/util/stack_util.cpp b/be/src/util/stack_util.cpp
index d8613cc1a5f..424db540cbb 100644
--- a/be/src/util/stack_util.cpp
+++ b/be/src/util/stack_util.cpp
@@ -17,22 +17,10 @@
 
 #include "util/stack_util.h"
 
-#include <execinfo.h>
-#include <signal.h>
-#include <stdio.h>
-
-#include <boost/stacktrace.hpp>
-
 #include "common/stack_trace.h"
 #include "util/mem_info.h"
 #include "util/pretty_printer.h"
 
-namespace google {
-namespace glog_internal_namespace_ {
-void DumpStackTraceToString(std::string* stacktrace);
-}
-} // namespace google
-
 namespace doris {
 
 std::string get_stack_trace(int start_pointers_index, std::string 
dwarf_location_info_mode) {
@@ -45,49 +33,6 @@ std::string get_stack_trace(int start_pointers_index, 
std::string dwarf_location
         dwarf_location_info_mode = config::dwarf_location_info_mode;
     }
 
-    auto tool = config::get_stack_trace_tool;
-    if (tool == "glog") {
-        return get_stack_trace_by_glog();
-    } else if (tool == "boost") {
-        return get_stack_trace_by_boost();
-    } else if (tool == "glibc") {
-        return get_stack_trace_by_glibc();
-    } else if (tool == "libunwind") {
-#if defined(__APPLE__) // TODO
-        return get_stack_trace_by_glog();
-#endif
-        return get_stack_trace_by_libunwind(start_pointers_index, 
dwarf_location_info_mode);
-    } else {
-        return "no stack";
-    }
-}
-
-std::string get_stack_trace_by_glog() {
-    std::string s;
-    google::glog_internal_namespace_::DumpStackTraceToString(&s);
-    return s;
-}
-
-std::string get_stack_trace_by_boost() {
-    return boost::stacktrace::to_string(boost::stacktrace::stacktrace());
-}
-
-std::string get_stack_trace_by_glibc() {
-    void* trace[16];
-    char** messages = (char**)nullptr;
-    int i, trace_size = 0;
-
-    trace_size = backtrace(trace, 16);
-    messages = backtrace_symbols(trace, trace_size);
-    std::stringstream out;
-    for (i = 1; i < trace_size; ++i) {
-        out << messages[i] << "\n";
-    }
-    return out.str();
-}
-
-std::string get_stack_trace_by_libunwind(int start_pointers_index,
-                                         const std::string& 
dwarf_location_info_mode) {
     return "\n" + StackTrace().toString(start_pointers_index, 
dwarf_location_info_mode);
 }
 
diff --git a/be/src/util/stack_util.h b/be/src/util/stack_util.h
index 3f406da707d..82f34c0b380 100644
--- a/be/src/util/stack_util.h
+++ b/be/src/util/stack_util.h
@@ -23,38 +23,10 @@ namespace doris {
 
 /** Returns the stack trace as a string from the current location.
   */
-
-// Select a stack trace tool according to config::get_stack_trace_tool
-// glog: 1000 times cost 8min, no line numbers.
-// boost: 1000 times cost 1min, has line numbers, but has memory leak.
-// glibc: 1000 times cost 1min, no line numbers, unresolved backtrace symbol.
-// libunwind: cost is negligible, has line numbers.
+// Doris uses only the libunwind-backed StackTrace implementation here. 
Keeping multiple runtime
+// stack walkers makes reliability depend on paths that are not validated by 
the BE stack-trace
+// deadlock tests.
 std::string get_stack_trace(int start_pointers_index = 0,
                             std::string dwarf_location_info_mode = "");
 
-// Note: there is a libc bug that causes this not to work on 64 bit machines
-// for recursive calls.
-std::string get_stack_trace_by_glog();
-
-// `boost::stacktrace::stacktrace()` has memory leak, reason for the 
boost::stacktrace memory leak
-// is that a state is saved in the thread local of each thread but is not 
actively released. Refer to:
-// https://github.com/boostorg/stacktrace/issues/118
-// https://github.com/boostorg/stacktrace/issues/111
-std::string get_stack_trace_by_boost();
-
-// backtrace symbol no parsing with Addr2Line, this is slower and often make 
mistakes, requiring manual parsing
-// 
https://stackoverflow.com/questions/3151779/best-way-to-invoke-gdb-from-inside-program-to-print-its-stacktrace/4611112#4611112
-// 
https://stackoverflow.com/questions/55450932/how-ro-resolve-cpp-symbols-from-backtrace-symbols-in-the-offset-during-runtime
-std::string get_stack_trace_by_glibc();
-
-// use StackTraceCache, PHDRCache speed up, is customizable and has some 
optimizations.
-// TODO:
-//  1. currently support linux __x86_64__, __arm__, __powerpc__, not supported 
__FreeBSD__, APPLE
-//     Note: __arm__, __powerpc__ not been verified
-//  2. Support signal handle
-//  3. libunwid support unw_backtrace for jemalloc
-//  4. Use of undefined compile option USE_MUSL for later
-std::string get_stack_trace_by_libunwind(int start_pointers_index,
-                                         const std::string& 
dwarf_location_info_mode);
-
 } // namespace doris
diff --git a/be/test/CMakeLists.txt b/be/test/CMakeLists.txt
index 95d2a435d8d..0609c20f3cb 100644
--- a/be/test/CMakeLists.txt
+++ b/be/test/CMakeLists.txt
@@ -45,6 +45,16 @@ file(GLOB_RECURSE UT_FILES CONFIGURE_DEPENDS
     vec/*.cpp
 )
 
+set(PHDR_CACHE_TEST_DSO_SOURCE 
${CMAKE_CURRENT_SOURCE_DIR}/common/phdr_cache_test_dso.cpp)
+list(REMOVE_ITEM UT_FILES ${PHDR_CACHE_TEST_DSO_SOURCE})
+
+if (NOT OS_MACOSX)
+    add_library(phdr_cache_test_dso SHARED ${PHDR_CACHE_TEST_DSO_SOURCE})
+    set_target_properties(phdr_cache_test_dso PROPERTIES
+        LIBRARY_OUTPUT_DIRECTORY "${BUILD_DIR}/test"
+    )
+endif()
+
 if (ENABLE_TDE)
     file(GLOB_RECURSE TDE_UT_FILES CONFIGURE_DEPENDS 
"${CMAKE_CURRENT_SOURCE_DIR}/${TDE_MODULE_DIR}/*.cpp")
     list(APPEND UT_FILES ${TDE_UT_FILES})
@@ -153,6 +163,9 @@ include_directories(
 add_subdirectory(storage/index/ann)
 
 add_executable(doris_be_test ${UT_FILES})
+if (TARGET phdr_cache_test_dso)
+    add_dependencies(doris_be_test phdr_cache_test_dso)
+endif()
 
 if (APPLE)
     target_link_libraries(doris_be_test ${TEST_LINK_LIBS}
diff --git a/be/test/common/phdr_cache_test.cpp 
b/be/test/common/phdr_cache_test.cpp
new file mode 100644
index 00000000000..c965d075044
--- /dev/null
+++ b/be/test/common/phdr_cache_test.cpp
@@ -0,0 +1,103 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#if defined(__linux__) && !defined(THREAD_SANITIZER) && !defined(USE_MUSL)
+
+#include "common/phdr_cache.h"
+
+#include <dlfcn.h>
+#include <link.h>
+
+#include <cstdlib>
+#include <filesystem>
+#include <string>
+#include <string_view>
+
+namespace {
+
+constexpr std::string_view TEST_DSO_NAME = "libphdr_cache_test_dso";
+
+int find_test_dso(dl_phdr_info* info, size_t /*size*/, void* data) {
+    auto* found = reinterpret_cast<bool*>(data);
+    if (info->dlpi_name != nullptr &&
+        std::string_view(info->dlpi_name).find(TEST_DSO_NAME) != 
std::string_view::npos) {
+        *found = true;
+        return 1;
+    }
+    return 0;
+}
+
+bool phdr_contains_test_dso() {
+    bool found = false;
+    dl_iterate_phdr(find_test_dso, &found);
+    return found;
+}
+
+bool unwind_phdr_cache_contains_test_dso(uintptr_t ip) {
+    bool found = false;
+    doris_unwind_iterate_phdr(find_test_dso, &found, ip);
+    return found;
+}
+
+std::string test_dso_path() {
+    const char* doris_home = std::getenv("DORIS_HOME");
+    std::filesystem::path path = doris_home == nullptr ? "." : doris_home;
+    path /= "libphdr_cache_test_dso.so";
+    return path.string();
+}
+
+} // namespace
+
+// Covers the exact late-dlopen risk of PHDR caching. Normal callers of 
dl_iterate_phdr must keep
+// seeing the live loader list, while the stack-trace signal handler can 
explicitly opt in to the
+// cached snapshot to avoid re-entering glibc's loader lock from an 
interrupted thread.
+TEST(PhdrCacheTest, DefaultLoaderViewIsLiveWhileScopedViewUsesSnapshot) {
+    updatePHDRCache();
+    ASSERT_TRUE(hasPHDRCache());
+    ASSERT_FALSE(phdr_contains_test_dso()) << "test DSO was loaded before the 
snapshot";
+
+    void* handle = dlopen(test_dso_path().c_str(), RTLD_NOW | RTLD_LOCAL);
+    ASSERT_NE(nullptr, handle) << dlerror();
+    using MarkerFunction = int (*)();
+    auto* marker = reinterpret_cast<MarkerFunction>(dlsym(handle, 
"phdr_cache_test_dso_marker"));
+    ASSERT_NE(nullptr, marker) << dlerror();
+    // Keep the handle open. Several sanitizer configurations are known to be 
fragile around
+    // dlclose(), and this test only needs a late-loaded object in the process 
loader list.
+    (void)handle;
+
+    EXPECT_TRUE(phdr_contains_test_dso())
+            << "default dl_iterate_phdr must use glibc's live loader list";
+
+    {
+        ScopedPHDRCacheRead cache_scope;
+        EXPECT_FALSE(phdr_contains_test_dso())
+                << "scoped PHDR cache should read the pre-dlopen snapshot";
+    }
+    
EXPECT_FALSE(unwind_phdr_cache_contains_test_dso(reinterpret_cast<uintptr_t>(marker)))
+            << "libunwind PHDR hook should also read the pre-dlopen snapshot";
+
+    updatePHDRCache();
+    
EXPECT_TRUE(unwind_phdr_cache_contains_test_dso(reinterpret_cast<uintptr_t>(marker)))
+            << "libunwind PHDR hook should use the refreshed snapshot without 
scoped opt-in";
+    {
+        ScopedPHDRCacheRead cache_scope;
+        EXPECT_TRUE(phdr_contains_test_dso())
+                << "refreshed scoped PHDR cache should include the late-loaded 
DSO";
+    }
+}
+
+#endif
diff --git a/be/test/common/phdr_cache_test_dso.cpp 
b/be/test/common/phdr_cache_test_dso.cpp
new file mode 100644
index 00000000000..5e3b92285b3
--- /dev/null
+++ b/be/test/common/phdr_cache_test_dso.cpp
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+extern "C" int phdr_cache_test_dso_marker() {
+    return 42;
+}
diff --git a/be/test/service/http/be_thread_stack_action_test.cpp 
b/be/test/service/http/be_thread_stack_action_test.cpp
index 5c5a5d46771..393046fb916 100644
--- a/be/test/service/http/be_thread_stack_action_test.cpp
+++ b/be/test/service/http/be_thread_stack_action_test.cpp
@@ -39,10 +39,15 @@
 #endif
 
 #include "common/config.h"
+#include "common/phdr_cache.h"
 #include "common/stack_trace.h"
+#if defined(__ELF__) && !defined(__FreeBSD__)
+#include "common/symbol_index.h"
+#endif
 #include "service/http/ev_http_server.h"
 #include "service/http/http_client.h"
 #include "service/http/http_method.h"
+#include "util/dynamic_util.h"
 
 namespace doris {
 
@@ -249,11 +254,10 @@ TEST_F(BeThreadStackActionTest, 
ThreadIdSelectorSupportsSingleAndMultipleIds) {
     EXPECT_THAT(body, testing::HasSubstr("service_signal: "));
     EXPECT_THAT(body, testing::HasSubstr("thread_count: 2\n"));
     EXPECT_THAT(body, testing::HasSubstr("dwarf_location_info_mode: 
disabled\n"));
-    EXPECT_THAT(body,
-                testing::HasSubstr(
-                        "signal_handler_unwinder: "
-                        
"frame_pointer_with_coordinator_signal_context_libunwind_fallback\n"));
-    EXPECT_THAT(body, testing::HasSubstr("capture_method="));
+    EXPECT_THAT(body, testing::HasSubstr("signal_handler_unwinder: 
signal_context_libunwind\n"));
+    EXPECT_THAT(body, testing::HasSubstr("phdr_cache: true\n"));
+    EXPECT_THAT(body, 
testing::HasSubstr("capture_method=signal_context_libunwind"));
+    EXPECT_THAT(body, testing::HasSubstr("unwind_status="));
     EXPECT_THAT(body, testing::HasSubstr(thread_header(first.tid())));
     EXPECT_THAT(body, testing::HasSubstr(thread_header(second.tid())));
     EXPECT_THAT(body, testing::HasSubstr("summary: captured=2 skipped=0 
timed_out=0 "
@@ -277,8 +281,9 @@ TEST_F(BeThreadStackActionTest, TidAliasRemainsSupported) {
     ASSERT_EQ(200, http_status);
     EXPECT_THAT(body, testing::HasSubstr("thread_count: 1\n"));
     EXPECT_THAT(body, testing::HasSubstr(thread_header(marker.tid())));
-    EXPECT_THAT(body, testing::HasSubstr("capture_method="));
-    EXPECT_THAT(body, testing::HasSubstr("fp_status="));
+    EXPECT_THAT(body, 
testing::HasSubstr("capture_method=signal_context_libunwind"));
+    EXPECT_THAT(body, testing::HasSubstr("phdr_cache=true"));
+    EXPECT_THAT(body, testing::Not(testing::HasSubstr("fp_status=")));
     EXPECT_EQ(1, count_thread_headers(body));
 
     marker.stop();
@@ -312,7 +317,9 @@ TEST_F(BeThreadStackActionTest, 
BlockingReadSyscallIsCapturedByDefault) {
                     .ok());
     ASSERT_EQ(200, http_status);
     EXPECT_THAT(thread_result_line(body, reader.tid()), 
testing::HasSubstr("status=ok"));
-    EXPECT_THAT(thread_result_line(body, reader.tid()), 
testing::HasSubstr("capture_method="));
+    EXPECT_THAT(thread_result_line(body, reader.tid()),
+                testing::HasSubstr("capture_method=signal_context_libunwind"));
+    EXPECT_THAT(thread_result_line(body, reader.tid()), 
testing::HasSubstr("phdr_cache=true"));
     EXPECT_THAT(body, testing::HasSubstr("summary: captured=1 skipped=0 
timed_out=0 "
                                          "remote_signal_attempts=1\n"));
     EXPECT_FALSE(reader.read_finished())
@@ -347,6 +354,33 @@ TEST_F(BeThreadStackActionTest, 
BlockingReadSyscallCanBeSkippedExplicitly) {
     reader.stop();
 }
 
+// Covers the dynamic-library refresh hook used by UDF loading. 
Doris-controlled dlopen/dlclose
+// paths must refresh both PHDR and SymbolIndex snapshots before later 
diagnostic stack requests
+// depend on newly loaded or unloaded libraries.
+TEST_F(BeThreadStackActionTest, DynamicOpenRefreshesPhdrCache) {
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    auto symbol_index_before_open = SymbolIndex::instance();
+#endif
+    void* handle = nullptr;
+    Status status = dynamic_open("libm.so.6", &handle);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+    EXPECT_TRUE(hasPHDRCache());
+#if defined(__ELF__) && !defined(__FreeBSD__)
+    auto symbol_index_after_open = SymbolIndex::instance();
+    EXPECT_NE(symbol_index_before_open.get(), symbol_index_after_open.get())
+            << "Doris-controlled dlopen should publish a fresh SymbolIndex 
snapshot";
+#endif
+
+    dynamic_close(handle);
+    EXPECT_TRUE(hasPHDRCache());
+#if defined(__ELF__) && !defined(__FreeBSD__) && !defined(ADDRESS_SANITIZER) 
&& \
+        !defined(LEAK_SANITIZER)
+    auto symbol_index_after_close = SymbolIndex::instance();
+    EXPECT_NE(symbol_index_after_open.get(), symbol_index_after_close.get())
+            << "Doris-controlled dlclose should publish a fresh SymbolIndex 
snapshot";
+#endif
+}
+
 // Covers request validation for thread filters, timeout, symbolization mode, 
and the syscall-skip
 // flag, so invalid knobs fail before any thread is signaled.
 TEST_F(BeThreadStackActionTest, InvalidParamsReturnBadRequest) {
diff --git a/be/test/testutil/run_all_tests.cpp 
b/be/test/testutil/run_all_tests.cpp
index 492db957603..c4413f5add0 100644
--- a/be/test/testutil/run_all_tests.cpp
+++ b/be/test/testutil/run_all_tests.cpp
@@ -106,6 +106,7 @@ int main(int argc, char** argv) {
 
     google::ParseCommandLineFlags(&argc, &argv, false);
 
+    configureLibunwindPHDRCache();
     updatePHDRCache();
     try {
         int res = RUN_ALL_TESTS();
diff --git a/build.sh b/build.sh
index f9f05c1df45..ba76f3aa802 100755
--- a/build.sh
+++ b/build.sh
@@ -548,14 +548,6 @@ if [[ -z "${USE_BTHREAD_SCANNER}" ]]; then
     USE_BTHREAD_SCANNER='OFF'
 fi
 
-if [[ -z "${USE_UNWIND}" ]]; then
-    if [[ "${TARGET_SYSTEM}" != 'Darwin' ]]; then
-        USE_UNWIND='ON'
-    else
-        USE_UNWIND='OFF'
-    fi
-fi
-
 if [[ -z "${DISPLAY_BUILD_TIME}" ]]; then
     DISPLAY_BUILD_TIME='OFF'
 fi
@@ -687,7 +679,6 @@ echo "Get params:
     GLIBC_COMPATIBILITY                 -- ${GLIBC_COMPATIBILITY}
     USE_AVX2                            -- ${USE_AVX2}
     USE_LIBCPP                          -- ${USE_LIBCPP}
-    USE_UNWIND                          -- ${USE_UNWIND}
     STRIP_DEBUG_INFO                    -- ${STRIP_DEBUG_INFO}
     USE_JEMALLOC                        -- ${USE_JEMALLOC}
     USE_BTHREAD_SCANNER                 -- ${USE_BTHREAD_SCANNER}
@@ -844,7 +835,6 @@ if [[ "${BUILD_BE}" -eq 1 ]]; then
         
-DBUILD_FILE_CACHE_MICROBENCH_TOOL="${BUILD_FILE_CACHE_MICROBENCH_TOOL}" \
         -DBUILD_INDEX_TOOL="${BUILD_INDEX_TOOL}" \
         -DSTRIP_DEBUG_INFO="${STRIP_DEBUG_INFO}" \
-        -DUSE_UNWIND="${USE_UNWIND}" \
         -DDISPLAY_BUILD_TIME="${DISPLAY_BUILD_TIME}" \
         -DENABLE_PCH="${ENABLE_PCH}" \
         -DUSE_JEMALLOC="${USE_JEMALLOC}" \
diff --git a/cloud/CMakeLists.txt b/cloud/CMakeLists.txt
index b67907f2d1a..d4e0b346659 100644
--- a/cloud/CMakeLists.txt
+++ b/cloud/CMakeLists.txt
@@ -98,6 +98,13 @@ else()
 endif()
 message(STATUS "make test: ${MAKE_TEST}")
 
+if (OS_MACOSX)
+    set(USE_UNWIND OFF)
+else()
+    set(USE_UNWIND ON)
+endif()
+message(STATUS "USE_UNWIND is ${USE_UNWIND}")
+
 option(ENABLE_TLS "Enable TLS feature module" OFF)
 set(TLS_MODULE_DIR "" CACHE STRING "TLS feature module directory under 
cloud/src")
 if (ENABLE_TLS AND "${TLS_MODULE_DIR}" STREQUAL "")
diff --git a/cloud/cmake/thirdparty.cmake b/cloud/cmake/thirdparty.cmake
index 6b1a614b395..a383b263b9c 100644
--- a/cloud/cmake/thirdparty.cmake
+++ b/cloud/cmake/thirdparty.cmake
@@ -64,6 +64,9 @@ if (USE_JEMALLOC)
 else()
     add_thirdparty(tcmalloc WHOLELIBPATH ${GPERFTOOLS_HOME}/lib/libtcmalloc.a 
NOTADD)
 endif()
+if (USE_UNWIND)
+    add_thirdparty(libunwind LIBNAME "lib64/libunwind.a")
+endif()
 add_thirdparty(leveldb) # Required by brpc
 add_thirdparty(brpc LIB64)
 add_thirdparty(rocksdb) # For local storage mocking
diff --git a/run-be-ut.sh b/run-be-ut.sh
index 2e8bd7be8bf..8227976d7ce 100755
--- a/run-be-ut.sh
+++ b/run-be-ut.sh
@@ -309,14 +309,6 @@ if [[ -z "${ARM_MARCH}" ]]; then
     ARM_MARCH='armv8-a+crc'
 fi
 
-if [[ -z "${USE_UNWIND}" ]]; then
-    if [[ "$(uname -s)" != 'Darwin' ]]; then
-        USE_UNWIND='ON'
-    else
-        USE_UNWIND='OFF'
-    fi
-fi
-
 if [[ -z "${ENABLE_INJECTION_POINT}" ]]; then
     ENABLE_INJECTION_POINT='ON'
 fi
@@ -341,7 +333,6 @@ cd "${CMAKE_BUILD_DIR}"
     -DUSE_LIBCPP="${USE_LIBCPP}" \
     -DBUILD_META_TOOL=OFF \
     -DBUILD_FILE_CACHE_MICROBENCH_TOOL=OFF \
-    -DUSE_UNWIND="${USE_UNWIND}" \
     -DUSE_JEMALLOC=OFF \
     -DUSE_AVX2="${USE_AVX2}" \
     -DARM_MARCH="${ARM_MARCH}" \
diff --git a/thirdparty/build-thirdparty.sh b/thirdparty/build-thirdparty.sh
index c3caf766998..180333d150f 100755
--- a/thirdparty/build-thirdparty.sh
+++ b/thirdparty/build-thirdparty.sh
@@ -1620,8 +1620,38 @@ build_jemalloc_doris() {
     # It is not easy to remove `with-jemalloc-prefix`, which may affect the 
compatibility between third-party and old version codes.
     # Also, will building failed on Mac, it said can't find mallctl symbol. 
because jemalloc's default prefix on macOS is "je_", not "".
     # Maybe can use alias instead of overwrite.
-    CFLAGS="${cflags}" ../configure --prefix="${TP_INSTALL_DIR}" 
--with-install-suffix="_doris" "${WITH_LG_PAGE}" \
-        --with-jemalloc-prefix=je --enable-prof --disable-cxx --disable-libdl 
--disable-shared
+    if [[ "${KERNEL}" == 'Darwin' ]]; then
+        # Doris does not build GNU libunwind on macOS, and Apple/LLVM 
libunwind does not provide
+        # jemalloc's required unw_backtrace symbol. Keep macOS on its original 
profiler backtrace
+        # path instead of forcing a Linux-only libunwind configuration.
+        CFLAGS="${cflags}" \
+            ../configure --prefix="${TP_INSTALL_DIR}" \
+            --with-install-suffix="_doris" "${WITH_LG_PAGE}" \
+            --with-jemalloc-prefix=je --enable-prof \
+            --disable-cxx --disable-libdl --disable-shared
+    else
+        CPPFLAGS="-I${TP_INCLUDE_DIR}" CFLAGS="${cflags}" 
LDFLAGS="-L${TP_LIB_DIR}" \
+            LIBS="-llzma -lz" \
+            ../configure --prefix="${TP_INSTALL_DIR}" \
+            --with-install-suffix="_doris" "${WITH_LG_PAGE}" \
+            --with-jemalloc-prefix=je --enable-prof --enable-prof-libunwind \
+            --disable-prof-libgcc --disable-cxx --disable-libdl 
--disable-shared
+
+        # The stack trace API redirects dl_iterate_phdr to a PHDR cache. On 
glibc platforms,
+        # jemalloc heap profiling must not silently fall back to libgcc's 
_Unwind_Backtrace path,
+        # because that path can re-enter the loader-lock implementation while 
a sampled target
+        # thread is interrupted.
+        if ! grep -qE "result: prof-libunwind +: 1$" config.log; then
+            echo "ERROR: jemalloc prof-libunwind is not enabled; refusing 
libgcc-backed heap profiles." >&2
+            grep -E "result: prof-(libunwind|libgcc|gcc) +:" config.log >&2 || 
true
+            exit 1
+        fi
+        if grep -qE "result: prof-libgcc +: 1$" config.log; then
+            echo "ERROR: jemalloc prof-libgcc is enabled; heap profiling must 
use libunwind only." >&2
+            grep -E "result: prof-(libunwind|libgcc|gcc) +:" config.log >&2 || 
true
+            exit 1
+        fi
+    fi
 
     make -j "${PARALLEL}"
     make install
@@ -2124,6 +2154,8 @@ if [[ "${#packages[@]}" -eq 0 ]]; then
         thrift
         leveldb
         brpc
+        lzma
+        libunwind
         jemalloc_doris
         rocksdb
         krb5 # before cyrus_sasl
@@ -2147,7 +2179,6 @@ if [[ "${#packages[@]}" -eq 0 ]]; then
         mysql
         aws_sdk
         js_and_css
-        lzma
         xml2
         idn
         gsasl
@@ -2160,7 +2191,6 @@ if [[ "${#packages[@]}" -eq 0 ]]; then
         xxhash
         concurrentqueue
         fast_float
-        libunwind
         avx2neon
         libdeflate
         streamvbyte
diff --git a/thirdparty/download-thirdparty.sh 
b/thirdparty/download-thirdparty.sh
index b7eb5bd9634..b4d3e5b42d7 100755
--- a/thirdparty/download-thirdparty.sh
+++ b/thirdparty/download-thirdparty.sh
@@ -484,6 +484,20 @@ if [[ " ${TP_ARCHIVES[*]} " =~ " JEMALLOC_DORIS " ]]; then
     echo "Finished patching ${JEMALLOC_DORIS_SOURCE}"
 fi
 
+# patch libunwind so Doris can force GNU libunwind to use the BE PHDR cache
+# without changing ordinary dl_iterate_phdr callers.
+if [[ " ${TP_ARCHIVES[*]} " =~ " LIBUNWIND " ]]; then
+    if [[ "${LIBUNWIND_SOURCE}" = "libunwind-1.6.2" ]]; then
+        cd "${TP_SOURCE_DIR}/${LIBUNWIND_SOURCE}"
+        if [[ ! -f "${PATCHED_MARK}" ]]; then
+            patch -p1 <"${TP_PATCH_DIR}/libunwind-1.6.2-doris-phdr-cache.patch"
+            touch "${PATCHED_MARK}"
+        fi
+        cd -
+    fi
+    echo "Finished patching ${LIBUNWIND_SOURCE}"
+fi
+
 # patch hyperscan
 # https://github.com/intel/hyperscan/issues/292
 if [[ " ${TP_ARCHIVES[*]} " =~ " HYPERSCAN " ]]; then
diff --git a/thirdparty/patches/libunwind-1.6.2-doris-phdr-cache.patch 
b/thirdparty/patches/libunwind-1.6.2-doris-phdr-cache.patch
new file mode 100644
index 00000000000..e534780b725
--- /dev/null
+++ b/thirdparty/patches/libunwind-1.6.2-doris-phdr-cache.patch
@@ -0,0 +1,31 @@
+diff --git a/src/dwarf/Gfind_proc_info-lsb.c b/src/dwarf/Gfind_proc_info-lsb.c
+index 1d0d6a4..8f47463 100644
+--- a/src/dwarf/Gfind_proc_info-lsb.c
++++ b/src/dwarf/Gfind_proc_info-lsb.c
+@@ -47,6 +47,14 @@ struct table_entry
+ 
+ #ifndef UNW_REMOTE_ONLY
+ 
++/*
++ * Doris provides a lock-free PHDR snapshot for GNU libunwind.  This avoids
++ * entering glibc dl_iterate_phdr from jemalloc profiling or signal-context
++ * unwinding while another thread is inside dlopen and holding the loader 
lock.
++ */
++extern int doris_unwind_iterate_phdr (int (*callback) (struct dl_phdr_info *, 
size_t, void *),
++                                      void *data, unw_word_t ip) 
__attribute__ ((weak));
++
+ #ifdef __linux__
+ #include "os-linux.h"
+ #endif
+@@ -804,7 +812,10 @@ dwarf_find_proc_info (unw_addr_space_t as, unw_word_t ip,
+   cb_data.di_debug.format = -1;
+ 
+   SIGPROCMASK (SIG_SETMASK, &unwi_full_mask, &saved_mask);
+-  ret = dl_iterate_phdr (dwarf_callback, &cb_data);
++  if (doris_unwind_iterate_phdr)
++    ret = doris_unwind_iterate_phdr (dwarf_callback, &cb_data, ip);
++  else
++    ret = dl_iterate_phdr (dwarf_callback, &cb_data);
+   SIGPROCMASK (SIG_SETMASK, &saved_mask, NULL);
+ 
+   if (ret > 0)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to