This is an automated email from the ASF dual-hosted git repository.
masaori335 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 4fabae0c72 Annotate BRAVO locks for thread-safety analysis (#13330)
4fabae0c72 is described below
commit 4fabae0c7299a45f4d448e9a8e6de0917b2416c9
Author: Masaori Koshiba <[email protected]>
AuthorDate: Fri Jun 26 07:52:16 2026 +0900
Annotate BRAVO locks for thread-safety analysis (#13330)
* Annotate BRAVO locks for thread-safety analysis
Enabling -Wthread-safety tree-wide surfaced findings in Bravo.h on
macOS, where libc++ marks the underlying std::shared_mutex as a
capability: the wrapper acquires and releases it across method
boundaries and on fast/slow-path branches the analyzer cannot follow.
Fedora CI uses libstdc++, which does not annotate std types, so it
never saw these.
Make ts::bravo::shared_mutex_impl a real capability so the contract is
checked on every Clang, not papered over on one std lib; its
lock-driving bodies are the trusted implementation and stay exempted,
matching the ts::shared_mutex pattern. Make the reader guard a rigid
scoped capability -- acquire in constructor, release in destructor,
no copy/move/defer/release -- which the analysis tracks with no
exemption; the movable std::shared_lock-style forms it replaced could
not be tracked, and no caller used them. Restructure the unit test's
try-lock asserts so the acquire gates a branch.
* Reset BRAVO Token on entry to harden slow-path unlock
lock_shared/try_lock_shared assigned the Token only on the fast path,
so a reused non-zero Token surviving into a slow-path acquisition would
make unlock_shared release a reader slot instead of the underlying
mutex. Clear it on entry to enforce the documented 0-init contract.
---
include/tsutil/Bravo.h | 140 +++++++++++-------------------------
src/tsutil/unit_tests/test_Bravo.cc | 35 ++++++---
2 files changed, 65 insertions(+), 110 deletions(-)
diff --git a/include/tsutil/Bravo.h b/include/tsutil/Bravo.h
index 4660f69aca..1dc1043d68 100644
--- a/include/tsutil/Bravo.h
+++ b/include/tsutil/Bravo.h
@@ -39,6 +39,7 @@
#include "tsutil/DenseThreadId.h"
#include "tsutil/Assert.h"
+#include "tsutil/ts_thread_safety.h"
#include <array>
#include <atomic>
@@ -68,129 +69,54 @@ using Token = size_t;
/**
ts::bravo::shared_lock
+
+ Reader guard for shared_mutex_impl, carrying the BRAVO Token. A rigid scoped
+ capability: it acquires the shared lock in its constructor and releases it
in
+ its destructor, with no copy, move, defer or release. That rigidity is what
+ lets the analysis track it -- the deferred and movable forms of
+ std::shared_lock let the held state escape a single scope and cannot be
+ modelled -- and BRAVO readers need only this scoped form.
*/
-template <class Mutex> class shared_lock
+template <class Mutex> class TS_SCOPED_CAPABILITY shared_lock
{
public:
using mutex_type = Mutex;
- shared_lock() noexcept = default;
- shared_lock(Mutex &m) : _mutex(&m) { lock(); }
- shared_lock(Mutex &m, std::try_to_lock_t) : _mutex(&m) { try_lock(); }
- shared_lock(Mutex &m, std::defer_lock_t) noexcept : _mutex(&m) {}
-
- ~shared_lock()
- {
- if (_owns) {
- _mutex->unlock_shared(_token);
- }
- };
+ explicit shared_lock(Mutex &m) TS_ACQUIRE_SHARED(m) : _mutex(&m) {
_mutex->lock_shared(_token); }
+ ~shared_lock() TS_RELEASE() { _mutex->unlock_shared(_token); }
////
- // Not Copyable
+ // Neither copyable nor movable: the held shared lock must not escape this
scope.
//
shared_lock(shared_lock const &) = delete;
shared_lock &operator=(shared_lock const &) = delete;
-
- ////
- // Moveable
- //
- shared_lock(shared_lock &&s) : _mutex(s._mutex), _token(s._token),
_owns(s._owns)
- {
- s._mutex = nullptr;
- s._token = 0;
- s._owns = false;
- };
-
- shared_lock &
- operator=(shared_lock &&s)
- {
- if (_owns) {
- _mutex->unlock_shared(_token);
- }
- _mutex = s._mutex;
- _token = s._token;
- _owns = s._owns;
-
- s._mutex = nullptr;
- s._token = 0;
- s._owns = false;
- };
-
- ////
- // Shared locking
- //
- void
- lock()
- {
- _mutex->lock_shared(_token);
- _owns = true;
- }
-
- bool
- try_lock()
- {
- _owns = _mutex->try_lock_shared(_token);
- return _owns;
- }
-
- // not implemented yet
- bool try_lock_for() = delete;
- bool try_lock_until() = delete;
-
- void
- unlock()
- {
- _mutex->unlock_shared(_token);
- _owns = false;
- }
-
- ////
- // Modifiers
- //
- void
- swap(shared_lock &s)
- {
- std::swap(_mutex, s._mutex);
- std::swap(_token, s._token);
- std::swap(_owns, s._owns);
- }
-
- mutex_type *
- release()
- {
- mutex_type *m = _mutex;
- _mutex = nullptr;
- _token = 0;
- _owns = false;
- return m;
- }
+ shared_lock(shared_lock &&) = delete;
+ shared_lock &operator=(shared_lock &&) = delete;
////
// Observers
//
mutex_type *
- mutex()
+ mutex() const
{
return _mutex;
}
Token
- token()
+ token() const
{
return _token;
}
bool
- owns_lock()
+ owns_lock() const
{
- return _owns;
+ return _mutex != nullptr;
}
private:
mutex_type *_mutex = nullptr;
Token _token = 0;
- bool _owns = false;
};
/**
@@ -201,7 +127,8 @@ private:
Set the SLOT_SIZE larger than DenseThreadId::num_possible_values to go
fast-path.
*/
-template <typename T = std::shared_mutex, size_t SLOT_SIZE = 256, int
SLOWDOWN_GUARD = 7> class shared_mutex_impl
+template <typename T = std::shared_mutex, size_t SLOT_SIZE = 256, int
SLOWDOWN_GUARD = 7>
+class TS_CAPABILITY("shared_mutex") shared_mutex_impl
{
public:
shared_mutex_impl() = default;
@@ -219,15 +146,22 @@ public:
////
// Exclusive locking
//
+ // These lock/unlock methods are the trusted implementation of this
capability:
+ // their bodies drive the underlying lock (T, by default std::shared_mutex,
+ // which libc++ annotates as a capability) across method boundaries and along
+ // the BRAVO fast/slow-path branches -- patterns the analysis cannot follow.
+ // Exempt the bodies so only the capability contract on each signature is
+ // checked; the data-race checking happens at the call sites.
+ //
void
- lock()
+ lock() TS_ACQUIRE() TS_NO_THREAD_SAFETY_ANALYSIS
{
_mutex.underlying.lock();
_revoke();
}
bool
- try_lock()
+ try_lock() TS_TRY_ACQUIRE(true) TS_NO_THREAD_SAFETY_ANALYSIS
{
bool r = _mutex.underlying.try_lock();
if (!r) {
@@ -240,19 +174,22 @@ public:
}
void
- unlock()
+ unlock() TS_RELEASE() TS_NO_THREAD_SAFETY_ANALYSIS
{
_mutex.underlying.unlock();
}
////
- // Shared locking
+ // Shared locking (bodies exempted for the same reason as the exclusive ones
above)
//
void
- lock_shared(Token &token)
+ lock_shared(Token &token) TS_ACQUIRE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS
{
debug_assert(SLOT_SIZE >= DenseThreadId::num_possible_values());
+ // Clear up front so a slow-path acquisition never leaves a stale
fast-path slot for unlock_shared().
+ token = 0;
+
// Fast path
if (_mutex.read_bias.load(std::memory_order_acquire)) {
size_t index = DenseThreadId::self() % SLOT_SIZE;
@@ -277,10 +214,13 @@ public:
}
bool
- try_lock_shared(Token &token)
+ try_lock_shared(Token &token) TS_TRY_ACQUIRE_SHARED(true)
TS_NO_THREAD_SAFETY_ANALYSIS
{
debug_assert(SLOT_SIZE >= DenseThreadId::num_possible_values());
+ // Clear up front so a slow-path acquisition never leaves a stale
fast-path slot for unlock_shared().
+ token = 0;
+
// Fast path
if (_mutex.read_bias.load(std::memory_order_acquire)) {
size_t index = DenseThreadId::self() % SLOT_SIZE;
@@ -313,7 +253,7 @@ public:
}
void
- unlock_shared(const Token token)
+ unlock_shared(const Token token) TS_RELEASE_SHARED()
TS_NO_THREAD_SAFETY_ANALYSIS
{
if (token == 0) {
_mutex.underlying.unlock_shared();
diff --git a/src/tsutil/unit_tests/test_Bravo.cc
b/src/tsutil/unit_tests/test_Bravo.cc
index 1a825cd946..666ae4f5cc 100644
--- a/src/tsutil/unit_tests/test_Bravo.cc
+++ b/src/tsutil/unit_tests/test_Bravo.cc
@@ -41,8 +41,11 @@ TEST_CASE("BRAVO - simple check", "[libts][BRAVO]")
std::thread t{[](ts::bravo::shared_mutex &mutex) {
ts::bravo::Token token{0};
- CHECK(mutex.try_lock_shared(token) == true);
- mutex.unlock_shared(token);
+ bool locked = mutex.try_lock_shared(token);
+ CHECK(locked == true);
+ if (locked) {
+ mutex.unlock_shared(token);
+ }
},
std::ref(mutex)};
@@ -95,28 +98,40 @@ TEST_CASE("BRAVO - multiple try-lock", "[libts][BRAVO]")
{
ts::bravo::Token token{0};
- CHECK(mutex.try_lock_shared(token));
+ bool locked = mutex.try_lock_shared(token);
+ CHECK(locked);
CHECK(i == 0);
- mutex.unlock_shared(token);
+ if (locked) {
+ mutex.unlock_shared(token);
+ }
}
{
- CHECK(mutex.try_lock());
+ bool locked = mutex.try_lock();
+ CHECK(locked);
CHECK(++i == 1);
- mutex.unlock();
+ if (locked) {
+ mutex.unlock();
+ }
}
{
ts::bravo::Token token{0};
- CHECK(mutex.try_lock_shared(token));
+ bool locked = mutex.try_lock_shared(token);
+ CHECK(locked);
CHECK(i == 1);
- mutex.unlock_shared(token);
+ if (locked) {
+ mutex.unlock_shared(token);
+ }
}
{
- CHECK(mutex.try_lock());
+ bool locked = mutex.try_lock();
+ CHECK(locked);
CHECK(++i == 2);
- mutex.unlock();
+ if (locked) {
+ mutex.unlock();
+ }
}
CHECK(i == 2);