Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/game/Debug/GdbServer/GdbBreakpoints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
#include "GdbMonitor.h"
#include "GdbServer.h"

#include <atomic>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <utility>
#include <vector>

Expand All @@ -50,8 +52,12 @@ namespace GdbBp
uint64 filter;
};

// g_entries is mutated on the world thread (monitor commands) but
// also iterated by Matches from foreign threads (network/DB call
// sites), so every access must hold g_entriesLock.
std::mutex g_entriesLock;
std::vector<Entry> g_entries;
uint64 g_hits = 0;
std::atomic<uint64> g_hits{0};

// Canonical lower-case name per Event, indexed by enum value. Keep in
// lockstep with the GdbEvent enum in shared/Debug/GdbEvents.h.
Expand All @@ -75,6 +81,7 @@ namespace GdbBp
static_cast<size_t>(Event::Count),
"kEventNames must match the Event enum");

// Callers must hold g_entriesLock; do not lock here (self-deadlock).
void Recount()
{
uint64 mask = 0;
Expand All @@ -93,6 +100,7 @@ namespace GdbBp

bool Matches(Event e, uint64 detail)
{
std::lock_guard<std::mutex> lock(g_entriesLock);
for (const Entry& entry : g_entries)
{
if (entry.ev == e && (entry.filter == 0 || entry.filter == detail))
Expand All @@ -105,6 +113,7 @@ namespace GdbBp

bool Arm(Event e, uint64 filter)
{
std::lock_guard<std::mutex> lock(g_entriesLock);
for (const Entry& entry : g_entries)
{
if (entry.ev == e && entry.filter == filter)
Expand All @@ -119,6 +128,7 @@ namespace GdbBp

bool Disarm(Event e, uint64 filter)
{
std::lock_guard<std::mutex> lock(g_entriesLock);
for (size_t i = 0; i < g_entries.size(); ++i)
{
if (g_entries[i].ev == e && g_entries[i].filter == filter)
Expand All @@ -133,12 +143,14 @@ namespace GdbBp

void DisarmAll()
{
std::lock_guard<std::mutex> lock(g_entriesLock);
g_entries.clear();
Recount();
}

void List(GdbMon::MonitorWriter& out)
{
std::lock_guard<std::mutex> lock(g_entriesLock);
if (g_entries.empty())
{
out.Str(" (no breakpoints armed)\n");
Expand All @@ -159,7 +171,7 @@ namespace GdbBp
out.Line();
}
out.Str(" hits=");
out.U64(g_hits);
out.U64(g_hits.load());
out.Line();
}

Expand Down
7 changes: 4 additions & 3 deletions src/game/Debug/GdbServer/GdbBreakpoints.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ namespace GdbMon { class MonitorWriter; }
* inspect game state via the `monitor` surface, then resume.
*
* The hot-path guard is a single relaxed atomic load of a per-event bitmask,
* so call sites cost effectively nothing when their event is not armed. All
* arming and matching happens on the world thread (monitor dispatch + game
* code), so the registry needs no locking.
* so call sites cost effectively nothing when their event is not armed.
* Arming happens on the world thread (monitor dispatch), but matching and
* hit-counting can also run from foreign threads (network/DB call sites), so
* the registry is serialised with a mutex and the hit counter is atomic.
*/
namespace GdbBp
{
Expand Down
24 changes: 23 additions & 1 deletion src/game/Debug/GdbServer/GdbServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,24 @@ void GdbServer::EnterStop(const char* reason)

void GdbServer::EnterBreak(const char* reason)
{
if (!m_enabled)
{
return;
}
// World-thread-only: EnterStop mutates unguarded RSP state and pauses
// the world by blocking this very thread. A breakpoint raised from the
// DB worker or an ACE network thread must not enter it; the hit was
// already counted by the caller, so just return. The acquire load pairs
// with the release store in OnWorldUpdate.
if (!m_worldThreadLatched.load(std::memory_order_acquire) ||
std::this_thread::get_id() != m_worldThreadId)
{
return;
}
// Only break when a debugger is attached — otherwise no one could resume
// the server and the world would hang. Never nest stops (a breakpoint may
// fire from a command executed while already stopped).
if (!m_enabled || m_inStop || !DebuggerAttached())
if (m_inStop || !DebuggerAttached())
{
return;
}
Expand All @@ -328,6 +342,14 @@ void GdbServer::OnWorldUpdate()
return;
}

// This is the world thread: latch its identity on the first tick so
// EnterBreak can reject breakpoints raised from foreign threads.
if (!m_worldThreadLatched.load(std::memory_order_acquire))
{
m_worldThreadId = std::this_thread::get_id();
m_worldThreadLatched.store(true, std::memory_order_release);
}

if (m_resetPending.exchange(false))
{
GdbRsp::ResetSession();
Expand Down
6 changes: 6 additions & 0 deletions src/game/Debug/GdbServer/GdbServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

/**
Expand Down Expand Up @@ -136,6 +137,11 @@ class GdbServer
// True while inside the stop loop; blocks a breakpoint that fires from
// a command run during the stop from nesting another stop.
bool m_inStop = false;
// World-thread identity, latched on the first OnWorldUpdate tick so
// EnterBreak can reject breakpoints raised from foreign threads. The
// id is written once before the latch flag is released.
std::atomic<bool> m_worldThreadLatched{false};
std::thread::id m_worldThreadId;
// Set by AttachRsp (network thread); consumed by OnWorldUpdate so all
// RSP engine state mutation happens on the world thread.
std::atomic<bool> m_resetPending{false};
Expand Down