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]

Reply via email to