Skip to content

[ASan] Prevent ASan/LSan deadlock by preloading modules before error reporting #131756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions compiler-rt/lib/asan/asan_report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ class ScopedInErrorReport {
public:
explicit ScopedInErrorReport(bool fatal = false)
: halt_on_error_(fatal || flags()->halt_on_error) {
// Deadlock Prevention Between ASan and LSan
//
// Background:
// - The `dl_iterate_phdr` function requires holding libdl's internal lock
// (Lock A).
// - LSan acquires the ASan thread registry lock (Lock B) *after* calling
// `dl_iterate_phdr`.
//
// Problem Scenario:
// When ASan attempts to call `dl_iterate_phdr` while holding Lock B (e.g.,
// during error reporting via `ErrorDescription::Print`), a circular lock
// dependency may occur:
// 1. Thread 1: Holds Lock B → Requests Lock A (via dl_iterate_phdr)
// 2. Thread 2: Holds Lock A → Requests Lock B (via LSan operations)
//
// Solution:
// Proactively load all required modules before acquiring Lock B.
// This ensures:
// 1. Any `dl_iterate_phdr` calls during module loading complete before
// locking.
// 2. Subsequent error reporting avoids nested lock acquisition patterns.
// 3. Eliminates the lock order inversion risk between libdl and ASan's
// thread registry.
Symbolizer::GetOrInit()->GetRefreshedListOfModules();

// Make sure the registry and sanitizer report mutexes are locked while
// we're printing an error report.
// We can lock them only here to avoid self-deadlock in case of
Expand Down
72 changes: 72 additions & 0 deletions compiler-rt/test/asan/TestCases/asan_lsan_deadlock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Test for potential deadlock in LeakSanitizer+AddressSanitizer.
// REQUIRES: leak-detection
//
// RUN: %clangxx_asan -O0 %s -o %t
// RUN: %env_asan_opts=detect_leaks=1 not %run %t 2>&1 | FileCheck %s

/*
* Purpose: Verify deadlock prevention between ASan error reporting and LSan leak checking.
*
* Test Design:
* 1. Creates contention scenario between:
* - ASan's error reporting (requires lock B -> lock A ordering)
* - LSan's leak check (requires lock A -> lock B ordering)
* 2. Thread timing:
* - Main thread: Holds 'in' mutex -> Triggers LSan check (lock A then B)
* - Worker thread: Triggers ASan OOB error (lock B then A via symbolization)
*
* Deadlock Condition (if unfixed):
* Circular lock dependency forms when:
* [Main Thread] LSan: lock A -> requests lock B
* [Worker Thread] ASan: lock B -> requests lock A
*
* Success Criteria:
* With proper lock ordering enforcement, watchdog should NOT trigger - test exits normally.
* If deadlock occurs, watchdog terminates via _exit(1) after 10s timeout.
*/

#include <mutex>
#include <sanitizer/lsan_interface.h>
#include <stdio.h>
#include <thread>
#include <unistd.h>

void Watchdog() {
// Safety mechanism: Turn infinite deadlock into finite test failure
usleep(10000000);
// CHECK-NOT: Timeout! Deadlock detected.
puts("Timeout! Deadlock detected.");
fflush(stdout);
_exit(1);
}

int main(int argc, char **argv) {
int arr[1] = {0};
std::mutex in;
in.lock();

std::thread w(Watchdog);
w.detach();

std::thread t([&]() {
in.unlock();
/*
* Provoke ASan error: ASan's error reporting acquires:
* 1. ASan's thread registry lock (B) during the reporting
* 2. dl_iterate_phdr lock (A) during symbolization
*/
// CHECK: SUMMARY: AddressSanitizer: stack-buffer-overflow
arr[argc] = 1; // Deliberate OOB access
});

in.lock();
/*
* Critical section: LSan's check acquires:
* 1. dl_iterate_phdr lock (A)
* 2. ASan's thread registry lock (B)
* before Stop The World.
*/
__lsan_do_leak_check();
t.join();
return 0;
}