TSan: data race on CSteamNetworkConnectionBase::m_eConnectionState via the unlocked GetState() fast path in InternalGetConnectionByHandle
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- cpp
- Ambito
- networking
Direzione di ricerca
Inizia in src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.h, in corrispondenza di CSteamNetworkConnectionBase::GetState() e m_eConnectionState, quindi esamina SetState() in steamnetworkingsockets_connections.cpp e il fast-path in csteamnetworkingsockets.cpp. Mantieni il comportamento della ricerca senza lock rendendo al contempo l’accesso allo stato privo di race, e verifica con la riproduzione segnalata di ThreadSanitizer che il report della data race non compaia più.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
InternalGetConnectionByHandle() reads the connection state before the connection lock has been taken, as a fast-path bail-out. The comment there says this is intentional:
src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp (master, L317-323):
// Fetch the state of the connection. This is OK to do
// even if we don't have the lock. If the connection
// is already dead we can avoid even trying to take
// the lock. That's good because cleaning up is one
// of the rare cases where we take locks in the opposite
// order, so we want to avoid that.
ESteamNetworkingConnectionState s = pResult->GetState();
GetState() is a plain read of a plain member:
src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.h L421 and L897:
ESteamNetworkingConnectionState GetState() const { return m_eConnectionState; }
...
ESteamNetworkingConnectionState m_eConnectionState;
Meanwhile SetState() writes that same member from the SteamNetworkingThreadProc service thread while holding the global and connection locks — which the reader above does not hold at the point of the read.
The intent is clearly benign (the value is only a fast-path hint, and it is re-read under the connection lock once that lock is held). But because the member is a non-atomic object, the read/write pair is a data race under the C++ memory model, and ThreadSanitizer reports it as one.
I am not asking for the locking to change — the fast-path read looks deliberate and well-motivated. The narrow fix would be to make the member a std::atomic accessed with memory_order_relaxed, which preserves the fast path, costs nothing on any supported target, and removes the UB and the tool report.
Report
Reproduced under Clang 21.1.8 -fsanitize=thread on Linux (AlmaLinux 10.2, x86-64), against v1.6.0 (2cb93a06350bb065db53abdb0d87cf297e0bfd34), built from source via FetchContent. Paths abbreviated to <GNS>; the application is a small authoritative game server whose only frame in the report is the flat-API SendMessages call.
WARNING: ThreadSanitizer: data race (pid=10260)
Write of size 4 at 0x728c00001a78 by thread T2 (mutexes: write M0, write M1):
#0 CSteamNetworkConnectionBase::SetState(ESteamNetworkingConnectionState, long long)
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp:2449
#1 CSteamNetworkConnectionBase::ConnectionState_ClosedByPeer(int, char const*)
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp:3263
#2 CConnectionTransportUDPBase::Received_ConnectionClosed(...)
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp:922
#3 CConnectionTransportUDP::PacketReceived(...)
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp:1453
#4 CRecvPacketCallback::operator()(...) <GNS>/.../steamnetworkingsockets_lowlevel.h:100
#5 CSharedSocket::DefaultCallbackRecvPacket(...)
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_socketthread.cpp:3590
#6 CRecvPacketCallback::operator()(...) <GNS>/.../steamnetworkingsockets_lowlevel.h:100
#7 DrainSocket(CRawUDPSocketImpl*) <GNS>/.../steamnetworkingsockets_socketthread.cpp:2575
#8 PollRawUDPSockets(int, bool) <GNS>/.../steamnetworkingsockets_socketthread.cpp:2724
#9 SteamNetworkingSockets_InternalPoll(int, bool)
<GNS>/.../steamnetworkingsockets_socketthread.cpp:3320
(called from SteamNetworkingThreadProc, <GNS>/.../steamnetworkingsockets_socketthread.cpp:3438)
Previous read of size 4 at 0x728c00001a78 by thread T1 (mutexes: write M2):
#0 CSteamNetworkConnectionBase::GetState() const
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.h:421
#1 InternalGetConnectionByHandle(unsigned int, ConnectionScopeLock&, char const*, bool)
<GNS>/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp:323
#2 GetConnectionByHandleForAPI(unsigned int, ConnectionScopeLock&, char const*)
<GNS>/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp:374
#3 CSteamNetworkingSockets::SendMessages(int, SteamNetworkingMessage_t**, long long*, bool)
<GNS>/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp:1354
#4 SteamAPI_ISteamNetworkingSockets_SendMessages
<GNS>/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_flat.cpp:70
#5 <application>::send(...) <- the only application frame in the report
SUMMARY: ThreadSanitizer: data race
.../steamnetworkingsockets_connections.cpp:2449
in SteamNetworkingSocketsLib::CSteamNetworkConnectionBase::SetState(...)
M2 on the reader is the table lock (g_tables_lock); M0/M1 on the writer are the global lock and the connection lock. The reader's line-323 access is the one taken before scopeLock.TryLock( *pResult->m_pLock, ... ) succeeds, so the two accesses share no lock, exactly as the comment describes.
Reproduction
Nothing exotic is required — just enough concurrent API traffic that a SendMessages() call overlaps a peer-initiated close.
- Build GNS with
-fsanitize=thread(the application and GNS both instrumented). - Run a server that calls
SteamAPI_ISteamNetworkingSockets_SendMessageson a single application thread every tick, for every connected peer. - Connect several clients and have them disconnect while traffic is in flight.
In our case the report appeared reliably once the send rate rose: at ~12 outbound packets per run it never fired; at ~18,800 it fires on essentially every run. The race itself does not depend on the send rate, only the chance of catching the window does.
Suggested fix
// steamnetworkingsockets_connections.h
std::atomic<ESteamNetworkingConnectionState> m_eConnectionState;
ESteamNetworkingConnectionState GetState() const
{ return m_eConnectionState.load( std::memory_order_relaxed ); }
with the corresponding relaxed store in SetState(). This keeps the unlocked fast-path read that the comment is defending, keeps the re-check under the connection lock, generates the same instruction on every supported target, and makes the access well-defined so TSan stops reporting it.
If the read is intended to stay non-atomic, __tsan_acquire/__tsan_release annotations or a documented race_top: suppression entry shipped with the project would at least let downstream users tell this apart from their own bugs. Right now every application that runs GNS under ThreadSanitizer has to independently rediscover and characterize this, and decide on its own whether it is looking at a library-internal design choice or at a bug in its own code.
Environment
| GNS | v1.6.0, 2cb93a06350bb065db53abdb0d87cf297e0bfd34 (behaviour verified unchanged on master) |
| Compiler | Clang 21.1.8, -fsanitize=thread |
| OS | AlmaLinux 10.2, x86-64, WSL2 kernel 6.18 |
| Build | FetchContent, static OSS direct-IP client library, no ICE |
- Lingua principale
- C++
- Stelle
- 9.9k
- Fork
- 749
- Merge medio
- 1g 2h
- PR unite (30g)
- 1
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ValveSoftware/GameNetworkingSockets
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
-
TSan: data race on the unsynchronized static FILE* lazy init in Plat_IsInDebugSession (Linux) Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
ValveSoftware/GameNetworkingSockets#425 · 7 commenti ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 45/100
Tutte le issue di ValveSoftware/GameNetworkingSockets
Issue simili
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
games-on-whales/wolf#509 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
-
bug-unconfirmed
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
NVIDIA/cuda-samples#453 ·