Files
ServerCore/TCPServer.h
T
Matt Arant d058322cc6 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).
2026-09-08 02:24:09 +00:00

226 lines
8.5 KiB
C++

#ifndef __TCPServer_h__
# define __TCPServer_h__
# include "Command.h"
# include "CommandList.h"
# include "IPAddressList.h"
# include "Socket.h"
# include "SubscriptionManager.h"
# include "TCPSession.h"
# include "TCPSocket.h"
# include "TLSInfo.h"
# include <mutex>
# include <set>
namespace core {
///
/// TCPServer
///
/// Manage a socket connection as a TCP server type. Connections to the socket are processed through
/// the accept functionality.
///
/// A list of connections is maintained in a vector object.
///
/// This object extends the Command object as well so it can be added to a Console object and
/// process commands to display status information.
///
class TCPServer : public TCPSocket, public Command {
public:
///
/// The constructor for the TCPServer object.
///
/// @param ePoll the EPoll instance that manages the socket.
/// @param url the IP address for the socket to receive connection requests.
/// @param port the port number that the socket will listen on.
/// @param commandName the name of the command used to invoke the status display for this object.
///
TCPServer(EPoll &ePoll, IPAddress address, TLSInfo *tlsInfo, std::string delimiter = " ", int depth = 10, std::string text = "");
///
/// The destructor for this object.
///
virtual ~TCPServer();
virtual void sessionErrorHandler(std::string errorString, std::stringstream &out);
///
/// getSocketAccept is designed to allow a polymorphic extension of this object to
/// return a type of object that extends the definition of the server socket.
/// Returning the appropriate session object that extends from Session provides
/// the mechanism where the server can select the protocol dialog for the desired
/// service.
///
virtual TCPSession *getSocketAccept(EPoll &epoll);
///
/// The list of sessions that are currently open and being maintained by this object.
///
/// 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
/// parsed and run as data comes into the session.
///
CommandList commands;
///
/// If not NULL the blacklist object can be assigned to this server socket and the server
/// IP addresses connecting to the server attempting to accept a socket are contained in
/// this list then the connection is rejected and no accept is granted.
///
IPAddressList *blackList;
///
/// If not NULL the blacklist object can be assigned to this server socket and the server
/// IP addresses connecting to the server attempting to accept a socket are contained in
/// this list then the connection is rejected and no accept is granted.
///
IPAddressList *whiteList;
void removeFromSessionList(TCPSession *session);
void output(std::stringstream &out); ///< Output the consoles array to the console.
///
///
///
void sendToAll(std::stringstream &out);
///
/// Use this sendToAll method to output the contents of the out stream
/// to all the connections on the server excluding the sender session.
///
void sendToAll(std::stringstream &out, TCPSession &sender, SessionFilter filter);
///
/// Use this sendToAll method to output the contents of the out stream
/// to all the connections on the server excluding the sender session
/// and the entries identified by the passed in filter object.
///
void sendToAll(std::stringstream &out, TCPSession &sender);
///
/// The Subscription Manager tracks all subscriptions on the server.
///
SubscriptionManager subscriptions;
///
/// Use the getSessionByAlias to retrieve a session pointer by the value
/// 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.
///
std::shared_ptr<TCPSession> getSessionByAlias(void *alias);
protected:
///
/// This method is called by the lower socket when a connection request comes into
/// the listening and bound server port.
///
void connectionRequest() override;
///
/// The listening socket itself is never a TLS peer - accepting a
/// connection on it surfaces as a plain read() failing with ENOTCONN
/// (see Socket::receiveData(), which is what actually detects and
/// dispatches to connectionRequest() above). When this server is
/// TLS-configured, TCPSocket::receiveData()'s normal ctx-based
/// dispatch would instead route that same event through
/// TLS::receiveData()/SSL_read() against a listening fd that was
/// never SSL_accept()-ed at all (real per-connection sessions are
/// the only sockets TLS applies to) - confirmed via a real crash
/// (SIGSEGV inside SSL_read, called with `this` pointing at this
/// server object, not a session). Always using the plain path here,
/// regardless of whether tlsInfo is set, keeps connection-detection
/// working the same way with or without TLS configured.
///
void receiveData(coreutils::ZString &buffer) override;
///
/// This method is called when the Command associated with this object is requested
/// because a user has typed in the associated command name on a command entry line.
///
/// @param the session object to write the output to.
///
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();
static std::mutex &listeningSocketsMutex() {
static std::mutex m;
return m;
}
static std::set<void *> &listeningSockets() {
static std::set<void *> s;
return s;
}
};
}
#endif