Describe the bug
ccf::kv::Store::get_map_internal reads the maps container without holding maps_lock, when reached via get_map_unsafe() from a caller that does not hold that lock. ThreadSanitizer reports it as a data race.
src/kv/store.h:
ccf::pal::Mutex maps_lock;
Maps maps;
...
std::shared_ptr<AbstractMap> get_map(ccf::kv::Version v, const std::string& map_name)
{
std::lock_guard<ccf::pal::Mutex> mguard(maps_lock); // locked
return get_map_internal(v, map_name);
}
std::shared_ptr<AbstractMap> get_map_unsafe(
ccf::kv::Version v, const std::string& map_name) override
{
return get_map_internal(v, map_name); // caller must hold maps_lock
}
The _unsafe suffix, and the lock_map_set() / unlock_map_set() pair on AbstractStore, make the contract clear: callers of get_map_unsafe are required to hold maps_lock. At least one caller does not.
The likely path is apply_changes() in src/kv/apply_changes.h, which calls store->get_map_unsafe(current_v, map_name) in its new-map conflict check. Its caller CommittableTx::commit() only takes the lock when the transaction itself created maps:
const bool maps_created = !pimpl->created_maps.empty();
if (maps_created)
{
this->pimpl->store->lock_map_set();
}
so a transaction that creates no new maps reaches the read with no lock held, while a concurrent transaction that does create maps writes to maps under maps_lock.
To Reproduce
Intermittent, under the TSAN CI job. Observed on an unrelated PR (#8249, which changes only src/consensus/aft/):
WARNING: ThreadSanitizer: data race (pid=7742)
SUMMARY: ThreadSanitizer: data race CCF/build/CCF/src/kv/store.h:314:26 in
ccf::kv::Store::get_map_internal(unsigned long, std::string const&)
reported four times in one run, failing e2e_logging_http2.
Expected behavior
Every caller of get_map_unsafe holds maps_lock, or the read is otherwise synchronised.
Additional context
Worth checking whether the conditional lock_map_set() is the intended design. If the conflict check genuinely needs to observe the map set, it arguably needs the lock regardless of whether this transaction creates maps, since the race is against other transactions creating them.
Clang thread-safety analysis is already enabled for PAL mutexes (#8180). Annotating maps with CCF_GUARDED_BY(maps_lock) and get_map_internal with CCF_REQUIRES(maps_lock) would surface this at compile time and prevent recurrence, though it may flag further call sites.
Found while reviewing CI on #8249; unrelated to that change.
Describe the bug
ccf::kv::Store::get_map_internalreads themapscontainer without holdingmaps_lock, when reached viaget_map_unsafe()from a caller that does not hold that lock. ThreadSanitizer reports it as a data race.src/kv/store.h:The
_unsafesuffix, and thelock_map_set()/unlock_map_set()pair onAbstractStore, make the contract clear: callers ofget_map_unsafeare required to holdmaps_lock. At least one caller does not.The likely path is
apply_changes()insrc/kv/apply_changes.h, which callsstore->get_map_unsafe(current_v, map_name)in its new-map conflict check. Its callerCommittableTx::commit()only takes the lock when the transaction itself created maps:so a transaction that creates no new maps reaches the read with no lock held, while a concurrent transaction that does create maps writes to
mapsundermaps_lock.To Reproduce
Intermittent, under the
TSANCI job. Observed on an unrelated PR (#8249, which changes onlysrc/consensus/aft/):reported four times in one run, failing
e2e_logging_http2.Expected behavior
Every caller of
get_map_unsafeholdsmaps_lock, or the read is otherwise synchronised.Additional context
Worth checking whether the conditional
lock_map_set()is the intended design. If the conflict check genuinely needs to observe the map set, it arguably needs the lock regardless of whether this transaction creates maps, since the race is against other transactions creating them.Clang thread-safety analysis is already enabled for PAL mutexes (#8180). Annotating
mapswithCCF_GUARDED_BY(maps_lock)andget_map_internalwithCCF_REQUIRES(maps_lock)would surface this at compile time and prevent recurrence, though it may flag further call sites.Found while reviewing CI on #8249; unrelated to that change.