From cafb45da0135a97c39c8fc79aa54474e3ab2da31 Mon Sep 17 00:00:00 2001 From: Alexander van der Grinten Date: Fri, 20 Feb 2026 19:28:00 +0100 Subject: [PATCH] shared_mutex: Fix races around st_.store() The implementation of async::shared_mutex previously did atomic store()s to the state even in situations where the shared lock count can be changed concurrently. This led to a race condition. * In shared_lock(), we can simply drop the problematic store() since it only writes back the state that we are currently in anyway. * In shared_unlock(), we need a CAS loop to avoid this issue. --- include/async/mutex.hpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/include/async/mutex.hpp b/include/async/mutex.hpp index ca051e1..33c8149 100644 --- a/include/async/mutex.hpp +++ b/include/async/mutex.hpp @@ -362,10 +362,6 @@ namespace detail { // mutex_ protects against concurrent transitions from contention::contended. assert(st.c == contention::contended); self_->waiters_.push_back(this); - self_->st_.store( - state{.c = contention::contended, .shared_cnt = st.shared_cnt}, - std::memory_order_relaxed - ); return; } } @@ -546,19 +542,26 @@ namespace detail { // Only the owner ever transitions out of state::locked so we must be in state::contended. assert(st.c == contention::contended); + // We can decrease shared lock count in state::contended state even outside of the mutex, + // as long as we do not need to transition out of state::contended. + while (st.shared_cnt > 1) { + bool success = st_.compare_exchange_weak( + st, + state{.c = contention::contended, .shared_cnt = st.shared_cnt - 1}, + std::memory_order_release, + std::memory_order_relaxed + ); + if (success) + return; + } + assert(st.shared_cnt == 1); + node *next; { frg::unique_lock lock(mutex_); - // In contrast to unlock(), contended shared -> shared transitions do not wake. - if (st.shared_cnt > 1) { - st_.store( - state{.c = contention::contended, .shared_cnt = st.shared_cnt - 1}, - std::memory_order_release - ); - return; - } - assert(st.shared_cnt == 1); + // Note that the shared count cannot increase in state::contended. + // Hence, even after taking the mutex, we are the only owner of the mutex. // Otherwise, we would not be in state::contended. assert(!waiters_.empty());