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 ⦥
- }
- }
- 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]