This is an automated email from the ASF dual-hosted git repository.
yiguolei 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 6fcc619eb62 [fix](be) Avoid ASAN double-free during PHDR unwinding
(#68217)
6fcc619eb62 is described below
commit 6fcc619eb62cf0a5c91130dbebe3eccb5a5238a9
Author: HappenLee <[email protected]>
AuthorDate: Sun Sep 20 09:58:04 2026 +0800
[fix](be) Avoid ASAN double-free during PHDR unwinding (#68217)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #64093
Problem Summary:
With `ASAN_OPTIONS=fast_unwind_on_malloc=0`, an ASAN BE can abort before
`main`, including when running `doris_be --version`. OpenMP
initialization reached from OpenBLAS probes optional symbols with
`dlsym`. On affected glibc versions, a subsequent lookup frees the
previous `dlerror` string before clearing its pointer. ASAN's free
interceptor collects a slow stack, which reaches Doris' global
`dl_iterate_phdr` interposer. Its fallback calls `dlsym` again, reenters
the pending error cleanup and frees the same string twice.
Exclude only the global `dl_iterate_phdr` interposer from ASAN builds.
ASAN then uses the system implementation without reentering `dlsym`
through Doris. Keep the PHDR snapshot and dedicated
`doris_unwind_iterate_phdr` hook for GNU libunwind; non-ASAN
interposition is unchanged.
Add an ASAN subprocess regression that re-execs with slow unwinding
enabled and performs two failed symbol lookups without consuming
`dlerror` between them. Adapt the existing late-`dlopen` test to assert
that ASAN keeps the live system view even inside `ScopedPHDRCacheRead`,
while the dedicated GNU hook still uses the snapshot.
### Release note
Fix an ASAN BE startup double-free when allocation/free stack collection
uses slow unwinding.
### Check List (For Author)
- Test
- [ ] Regression test
- [ ] Unit Test execution: added a subprocess regression and updated the
PHDR cache test; full local execution is blocked by the installed AWS
SDK missing `aws/core/auth/GeneralHTTPCredentialsProvider.h` while
compiling unchanged `common/cpp/aws_common.cpp`.
- [x] Manual test:
- Compiled the actual pre-fix and fixed `phdr_cache.cpp` into the same
ASAN startup reproducer (two failed `dlsym` calls,
`fast_unwind_on_malloc=0`): pre-fix reports double-free; fixed reaches
`main`.
- OpenMP initialization from a pre-main constructor with the fixed
source and slow unwinding: passed.
- Live-loader versus PHDR snapshot checks in ASAN and non-ASAN
standalone probes, including a late-loaded DSO and cache refresh:
passed.
- Updated unit-test source syntax check: passed.
- clang-format 16, build hygiene and clang-tidy checks on all three
changed files: passed.
- Full local test command attempted: `./run-be-ut.sh -j 48 --run
--filter='PhdrCache*'` (ASAN; dependency failure noted above).
- Behavior changed:
- [x] Yes. ASAN uses the system `dl_iterate_phdr` even inside a scoped
cache read; its dedicated GNU libunwind hook remains cached. Non-ASAN
behavior is unchanged.
- Does this need documentation?
- [x] No. Internal sanitizer compatibility fix; the source API comment
documents the ASAN exception.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/common/phdr_cache.cpp | 5 +++++
be/src/common/phdr_cache.h | 2 ++
be/test/common/phdr_cache_test.cpp | 31 +++++++++++++++++++++++++++++++
3 files changed, 38 insertions(+)
diff --git a/be/src/common/phdr_cache.cpp b/be/src/common/phdr_cache.cpp
index afc2cfb5ab7..ddaad7c06cc 100644
--- a/be/src/common/phdr_cache.cpp
+++ b/be/src/common/phdr_cache.cpp
@@ -228,6 +228,10 @@ int iteratePHDRCache(int (*callback)(dl_phdr_info* info,
size_t size, void* data
} // namespace
+// ASAN's slow unwinder can reach this interposer while dlsym is freeing its
previous error
+// string. Resolving the original function with dlsym here would reenter that
cleanup and
+// free the same string twice. Keep ASAN on the system implementation instead.
+#if !defined(ADDRESS_SANITIZER)
extern "C"
#ifndef __clang__
[[gnu::visibility("default")]] [[gnu::externally_visible]]
@@ -240,6 +244,7 @@ extern "C"
return iteratePHDRCache(callback, data, 0);
}
+#endif
extern "C"
#ifndef __clang__
diff --git a/be/src/common/phdr_cache.h b/be/src/common/phdr_cache.h
index abf08a0500f..a2a47d56124 100644
--- a/be/src/common/phdr_cache.h
+++ b/be/src/common/phdr_cache.h
@@ -39,6 +39,8 @@
* 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.
+ * ASAN builds do not interpose dl_iterate_phdr, even inside
ScopedPHDRCacheRead, to avoid
+ * dlsym reentrancy during ASAN's slow unwinding. The dedicated GNU libunwind
hook still caches.
*
* Old cache snapshots are intentionally leaked and remain readable by
concurrent signal-handler
* unwinders.
diff --git a/be/test/common/phdr_cache_test.cpp
b/be/test/common/phdr_cache_test.cpp
index c965d075044..5d7d2cb4ded 100644
--- a/be/test/common/phdr_cache_test.cpp
+++ b/be/test/common/phdr_cache_test.cpp
@@ -20,6 +20,7 @@
#include "common/phdr_cache.h"
#include <dlfcn.h>
+#include <gtest/gtest.h>
#include <link.h>
#include <cstdlib>
@@ -62,6 +63,31 @@ std::string test_dso_path() {
} // namespace
+#if defined(ADDRESS_SANITIZER)
+TEST(PhdrCacheDeathTest, FailedSymbolLookupsWithSlowUnwinding) {
+ const char* asan_options = std::getenv("ASAN_OPTIONS");
+ const bool had_options = asan_options != nullptr;
+ const std::string saved_options = had_options ? asan_options : "";
+ const std::string child_options = saved_options +
":fast_unwind_on_malloc=0:disable_coredump=1";
+ ASSERT_EQ(0, setenv("ASAN_OPTIONS", child_options.c_str(), 1));
+
+ // Re-exec so ASAN reads the slow-unwind option before initializing the
child process.
+ ::testing::FLAGS_gtest_death_test_style = "threadsafe";
+ EXPECT_EXIT(
+ {
+ // Do not consume dlerror() between lookups: the second dlsym
must free the
+ // first lookup's error string while ASAN collects the free
stack trace.
+ void* first = dlsym(RTLD_DEFAULT,
"doris_phdr_cache_missing_symbol_one");
+ void* second = dlsym(RTLD_DEFAULT,
"doris_phdr_cache_missing_symbol_two");
+ std::_Exit(static_cast<int>(first != nullptr || second !=
nullptr));
+ },
+ ::testing::ExitedWithCode(0), "");
+
+ EXPECT_EQ(0, had_options ? setenv("ASAN_OPTIONS", saved_options.c_str(), 1)
+ : unsetenv("ASAN_OPTIONS"));
+}
+#endif
+
// 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.
@@ -84,8 +110,13 @@ TEST(PhdrCacheTest,
DefaultLoaderViewIsLiveWhileScopedViewUsesSnapshot) {
{
ScopedPHDRCacheRead cache_scope;
+#if defined(ADDRESS_SANITIZER)
+ EXPECT_TRUE(phdr_contains_test_dso())
+ << "ASAN must use the live loader list even inside a cache
scope";
+#else
EXPECT_FALSE(phdr_contains_test_dso())
<< "scoped PHDR cache should read the pre-dlopen snapshot";
+#endif
}
EXPECT_FALSE(unwind_phdr_cache_contains_test_dso(reinterpret_cast<uintptr_t>(marker)))
<< "libunwind PHDR hook should also read the pre-dlopen snapshot";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]