Move TCPSession lifetime to shared_ptr ownership, fix the whole double-delete/UAF class of crash
Every crash root-caused earlier tonight (Subscription::subscribers, TCPServer::sessions, the Socket::write()/eventReceived() re-arm race) was the same underlying shape: some thread calling a method - or Thread::run() calling delete - on a TCPSession* while another thread concurrently freed that same object, because nothing actually guaranteed the object stayed alive for the duration of a call. Mutex-based patches at each individual call site kept narrowing the window without closing it (confirmed live, repeatedly, via AddressSanitizer under concurrent load - each fix produced a new, different crash signature at the same underlying race), and adding more locks around every write() call also works against the fifo-based design's own goal: no thread should have to block just to hand a message to another thread's session. TCPSession now derives from std::enable_shared_from_this<TCPSession> and exposes pin() - a safe upgrade from a stored raw pointer to a real, ownership-extending shared_ptr, returning nullptr instead of throwing once the object's real owner has let go. TCPServer::sessions is now the one real strong-owning shared_ptr<TCPSession> per live session; every other place that stores a TCPSession* (Subscription::subscribers, Zephora's own sessionsByPlayerName, GroupObject's membership list, etc.) is just a non-owning observer and must pin() before touching the object. A second, distinct problem: Thread::run()'s epoll dispatch starts from a raw void* of unknown validity. An early version of this fix used dynamic_cast<TCPSession*> to identify session objects before calling pin() - confirmed live via AddressSanitizer that dynamic_cast itself segfaults on a stale pointer, since it has to read the (potentially freed) object's own vtable to work at all. TCPSession::tryGet()/TCPServer:: isKnownListeningSocket() replace that: both look the raw pointer up as a key in a registry, never dereferencing it for the check itself - safe to call on any value, live session, immortal listening socket, or fully freed garbage alike. Verified via an AddressSanitizer-instrumented build under sustained concurrent load (up to 15-way parallel connect/subscribe/create/disconnect cycles, multiple rounds, 100+ total connections) with zero errors, then against the real production build with a genuine 500-connection load test (463/500 succeeded; the rest hit the process's own file-descriptor ulimit under peak burst, an unrelated and separately deferred tuning matter, not a crash). Files touched: TCPSession.h (enable_shared_from_this, pin(), the tryGet() registry), TCPServer.h/.cpp (sessions as shared_ptr, isKnownListening Socket() registry, lock made protected for subclasses), Thread.cpp (the whole dispatch rewrite), Subscription.cpp/SubscriptionManager.cpp (every subscriber write() site now pin()s first), ConsoleServer.cpp (logSend() was iterating `sessions` with no lock at all - fixed alongside the type change it needed anyway).
This commit is contained in:
1 parent
8009564a7a
commit
d058322cc6
10 files changed
+411
-64
No files matched your search
+62
-6
@@ -9,6 +9,8 @@
|
||||
# include "TCPSession.h"
|
||||
# include "TCPSocket.h"
|
||||
# include "TLSInfo.h"
|
||||
# include <mutex>
|
||||
# include <set>
|
||||
|
||||
namespace core {
|
||||
|
||||
@@ -60,8 +62,20 @@ namespace core {
|
||||
///
|
||||
/// The list of sessions that are currently open and being maintained by this object.
|
||||
///
|
||||
|
||||
std::vector<TCPSession *> sessions;
|
||||
/// This is the one real, strong-owning reference per live session -
|
||||
/// see TCPSession::pin()'s own doc comment for the full reasoning.
|
||||
/// Every other place that keeps a TCPSession* around (Subscription::
|
||||
/// subscribers, a Zephora-specific sessionsByPlayerName map, etc.)
|
||||
/// is just a non-owning observer and must pin() before touching the
|
||||
/// object. A session stops existing, for real, the instant its
|
||||
/// entry here is erased and every other outstanding pin() (if any
|
||||
/// thread happened to be mid-call) has released its own reference -
|
||||
/// removeFromSessionList() is the only place that ever removes an
|
||||
/// entry, and Thread::run() calls it instead of ever calling
|
||||
/// `delete` on a TCPSession directly (see its own comment).
|
||||
///
|
||||
|
||||
std::vector<std::shared_ptr<TCPSession>> sessions;
|
||||
|
||||
///
|
||||
/// The commands object is a CommandList and is used to store Command objects to be
|
||||
@@ -119,10 +133,13 @@ namespace core {
|
||||
|
||||
///
|
||||
/// Use the getSessionByAlias to retrieve a session pointer by the value
|
||||
/// of the alias pointer.
|
||||
/// of the alias pointer. Returns an already-pin()'d shared_ptr (null
|
||||
/// if not found) rather than a raw pointer, so callers can't
|
||||
/// accidentally hold onto - or use - a reference that isn't actually
|
||||
/// keeping the session alive.
|
||||
///
|
||||
|
||||
TCPSession *getSessionByAlias(void *alias);
|
||||
|
||||
std::shared_ptr<TCPSession> getSessionByAlias(void *alias);
|
||||
|
||||
protected:
|
||||
///
|
||||
@@ -159,9 +176,48 @@ namespace core {
|
||||
|
||||
int processCommand(coreutils::ZString &request, TCPSession &session) override;
|
||||
|
||||
protected:
|
||||
// Was private - ConsoleServer::logSend() (and any other subclass
|
||||
// that iterates `sessions`, itself public further up) needs to be
|
||||
// able to synchronize against the exact same mutex
|
||||
// connectionRequest()/removeFromSessionList() already use, rather
|
||||
// than reading a shared_ptr vector with no locking of its own.
|
||||
std::mutex lock;
|
||||
|
||||
public:
|
||||
///
|
||||
/// A listening TCPServer/ConsoleServer (as opposed to an accepted
|
||||
/// TCPSession) is constructed once, directly by main(), and lives
|
||||
/// for the whole process - never through the shared_ptr-based
|
||||
/// ownership TCPSession::registerLive()/pin() provide (see their
|
||||
/// own comments), since nothing ever calls `new` on one of these.
|
||||
/// Thread::run()'s epoll dispatch still needs a SAFE way to tell
|
||||
/// "this raw pointer is one of those known-immortal listening
|
||||
/// sockets, fine to call methods on directly" apart from "this is
|
||||
/// an unrecognized/stale pointer that must not be touched at all"
|
||||
/// (see TCPSession::tryGet()'s own comment on why dynamic_cast
|
||||
/// can't make that distinction safely). Registering `this` once,
|
||||
/// in the constructor, into this tiny set is that signal - it's
|
||||
/// checked by address only, never dereferenced for the check
|
||||
/// itself.
|
||||
///
|
||||
|
||||
static bool isKnownListeningSocket(void *ptr) {
|
||||
std::lock_guard<std::mutex> guard(listeningSocketsMutex());
|
||||
return listeningSockets().count(ptr) > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
TCPSession *accept();
|
||||
std::mutex lock;
|
||||
|
||||
static std::mutex &listeningSocketsMutex() {
|
||||
static std::mutex m;
|
||||
return m;
|
||||
}
|
||||
static std::set<void *> &listeningSockets() {
|
||||
static std::set<void *> s;
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user