Issue 169743
Summary [libcxx] std::shared_mutex::unlock may result in use-after-free during notify on gates
Labels libc++
Assignees
Reporter es1024
    Consider the following (https://godbolt.org/z/sf9eo74Mj):
```c++
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <thread>

int main() {
  auto mutex = std::make_unique<std::shared_mutex>();
  bool done = false;
  
  std::thread t([&] {
    mutex->lock();
    done = true;
 mutex->unlock(); // (1)
  });

  while (true) {
    std::lock_guard lock(*mutex); // (3)
    if (done) {
      break;
    }
  }
 mutex.reset(); // (2)
  t.join();
}
```

This runs without warning on Clang 19/libc++/TSAN, but on Clang 20/libc++/TSAN, emits the following warning:
```
WARNING: ThreadSanitizer: data race (pid=1)
  Write of size 8 at 0x722400000028 by main thread:
    #0 pthread_cond_destroy /root/llvm-project/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp:1333:3 (output.s+0x655ea)
    #1 std::__1::__shared_mutex_base::~__shared_mutex_base[abi:ne200100]() /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/shared_mutex:166:56 (output.s+0xe8cba)
    #2 std::__1::shared_mutex::~shared_mutex[abi:ne200100]() /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/shared_mutex:191:49 (output.s+0xe8c65)
    #3 std::__1::default_delete<std::__1::shared_mutex>::operator()[abi:ne200100](std::__1::shared_mutex*) const /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/__memory/unique_ptr.h:78:5 (output.s+0xe8c1f)
    #4 std::__1::unique_ptr<std::__1::shared_mutex, std::__1::default_delete<std::__1::shared_mutex>>::reset[abi:ne200100](std::__1::shared_mutex*) /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/__memory/unique_ptr.h:300:7 (output.s+0xe8adc)
    #5 main /app/example.cpp:22:9 (output.s+0xe8047)

 Previous read of size 8 at 0x722400000028 by thread T1:
    #0 pthread_cond_broadcast /root/llvm-project/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp:1326:3 (output.s+0x65458)
    #1 std::__1::shared_mutex::unlock[abi:ne200100]() /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/shared_mutex:204:20 (output.s+0xe9075)
    #2 main::$_0::operator()() const /app/example.cpp:13:12 (output.s+0xe8701)
    #3 decltype(std::declval<main::$_0>()()) std::__1::__invoke[abi:ne200100]<main::$_0>(main::$_0&&) /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/__type_traits/invoke.h:179:25 (output.s+0xe8615)
    #4 void std::__1::__thread_execute[abi:ne200100]<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, main::$_0>(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, main::$_0>&, std::__1::__tuple_indices<...>) /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/__thread/thread.h:199:3 (output.s+0xe85cd)
    #5 void* std::__1::__thread_proxy[abi:ne200100]<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, main::$_0>>(void*) /cefs/d2/d2e6ebb9fe16525f6e7eb0c3_consolidated/compilers_c++_clang_20.1.0/bin/../include/c++/v1/__thread/thread.h:208:3 (output.s+0xe836c)
```
where the mutex destruction in (2) is considered to race against the mutex unlock in (1). No such warning is emitted if `std::shared_mutex` is replaced with `std::mutex`.

This seems to be a valid program; the standard has:
> The behavior of a program is undefined if
> - (3.1) it destroys a shared_mutex object owned by any thread,
> - (3.2) a thread attempts to recursively gain any ownership of a shared_mutex, or
> - (3.3) a thread terminates while possessing any ownership of a shared_mutex.

(3.2) and (3.3) are irrelevant for this case. 

In the above program, T1 must release ownership of the mutex at (1) before the iteration of (3) that observes `done == true` acquires ownership of mutex. The mutex is then released by the main thread before mutex destruction at (2), so there should be a guarantee that no thread owns the mutex at time of destruction, and thus (3.1) does not apply either.

The change in behavior from clang 19 -> 20 seems to be due to this commit: https://github.com/llvm/llvm-project/commit/ab6c89c220192159a66c1a91ad3dd892bad1c3b2 which moves `__gate1_.notify_all();` outside of mutex lock during `__shared_mutex_base::unlock`. But this means that the main thread in the above example may now lock the shared_mutex, observe `done == true`, unlock the shared_mutex, and destroy the shared_mutex, all before thread T1 calls `__gate1_.notify_all()`, on a now destroyed condition variable.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to