Files
ServerCore/TCPServer.cpp
T
Matt Arant 0bb926a264 Fix unsynchronized data race in TCPServer::sessions
connectionRequest()/removeFromSessionList() already lock `lock` around
every mutation of `sessions` (a session accepting or disconnecting), but
every read of it - sendToAll() (all three overloads), getSessionByAlias(),
and the debug-console processCommand() - iterated it with no lock at all.
Exact same bug shape as Subscription::subscribers, fixed earlier today in
SubscriptionManager.cpp, just in a completely separate container.

Confirmed live via dmesg: a real segfault at a stable, deterministic
instruction offset inside the binary, hit twice across separate process
launches tonight - Socket::write() -> resetSocket() dereferencing a null
EPoll reference, the same crash signature as the earlier Subscription fix,
consistent with iterating `sessions` while another thread concurrently
erased and freed one of its entries. sendToAll() backs most real gameplay
broadcasts (combat, position, chat), so this is very likely the actual
dominant cause of tonight's crash - not just the ItemDataBase-specific
subscriber list the earlier fix addressed.

Took the existing `lock` mutex around all five read sites. No new
reentrancy risk: SubscriptionManager's "invite" verb already calls
getSessionByAlias() while holding SubscriptionManager::lock, so the two
mutexes now nest in one fixed order (SubscriptionManager::lock, then
TCPServer::lock) - nothing acquires them in the reverse order anywhere
in this codebase, so no AB-BA deadlock risk.
2026-09-07 23:31:28 +00:00

185 lines
7.3 KiB
C++

#include "TCPServer.h"
#include "EPoll.h"
#include "Exception.h"
#include "Log.h"
#include "TCPSession.h"
namespace core {
TCPServer::TCPServer(EPoll &ePoll, IPAddress address, TLSInfo *tlsInfo, std::string delimiter, int depth, std::string text)
: TCPSocket(ePoll, tlsInfo, text), commands(delimiter, depth) {
// Wires the subscription (pub/sub) command set into this server's
// own command dispatch - previously missing entirely here (the
// subscriptions member existed but nothing ever routed these verbs
// to it), so publish/subscribe/catalog/etc were unreachable on any
// TCPServer built after the TLS work, even though SubscriptionManager
// itself was still fully implemented.
commands.add(subscriptions, "publish");
commands.add(subscriptions, "unpublish");
commands.add(subscriptions, "subscribe");
commands.add(subscriptions, "unsubscribe");
commands.add(subscriptions, "catalog");
commands.add(subscriptions, "event");
commands.add(subscriptions, "invite");
setDescriptor(socket(AF_INET, SOCK_STREAM, 0));
int yes = 1;
setsockopt(getDescriptor(), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
if (bind(getDescriptor(), address.getPointer(), address.addressLength) < 0)
throw coreutils::Exception("Error on bind to socket: " + std::to_string(errno));
if (listen(getDescriptor(), 20) < 0)
throw coreutils::Exception("Error on listen to socket");
}
TCPServer::~TCPServer() {
coreutils::Log(coreutils::LOG_DEBUG_2) << "Closing server socket " << getDescriptor() << ".";
close(getDescriptor());
}
void TCPServer::connectionRequest() {
coreutils::Log(coreutils::LOG_DEBUG_2) << "Connection request is being received on socket " << getDescriptor() << ".";
lock.lock();
TCPSession *session = accept();
if (session)
sessions.push_back(session);
// Must run ON THE SESSION, not `this` (the listening server) - the
// unqualified registerSocket()/acceptSocket() calls this replaced
// built the SSL* handshake state on the SERVER's own TLS-base
// member instead of the newly accepted session's, leaving every
// session's own `ssl` uninitialized and segfaulting the first time
// real data arrived on it (SSL_read() against a never-SSL_new()'d
// pointer - confirmed via gdb). Sessions already correctly inherit
// this server's SSL_CTX at construction (TCPSession's own
// TLS(SSL_CTX*)-taking constructor) - performTLSHandshake() just
// needed to actually be invoked on that object.
if (session && tlsInfo)
session->performTLSHandshake();
lock.unlock();
}
void TCPServer::receiveData(coreutils::ZString &buffer) {
// See this override's own doc comment in TCPServer.h - the
// listening socket always uses the plain read()/ENOTCONN detection
// path, never TLS::receiveData(), regardless of whether this server
// is TLS-configured.
Socket::receiveData(buffer);
}
TCPSession * TCPServer::accept() {
try
{
TCPSession *session = getSocketAccept(ePoll);
session->setDescriptor(::accept(getDescriptor(), (struct sockaddr *)&session->ipAddress.addr, &session->ipAddress.addressLength));
// if(blackList && blackList->contains(session->ipAddress.getClientAddress())) {
// session->shutdown();
// Log(LOG_WARN) << "Client at IP address " << session->ipAddress.getClientAddress() << " is blacklisted and was denied a connection.";
// return NULL;
// }
// if(whiteList && !whiteList->contains(session->ipAddress.getClientAddress())) {
// session->shutdown();
// Log(LOG_WARN) << "Client at IP address " << session->ipAddress.getClientAddress() << " is not authorized and was denied a connection.";
// return NULL;
// }
return session;
}
catch (coreutils::Exception e) {
coreutils::Log(coreutils::LOG_EXCEPT) << "Major error on session initialization. Error is '" << e.text << "'.";
}
catch (...) {
coreutils::Log(coreutils::LOG_EXCEPT) << "Unnspecified error on session initialization.";
}
return NULL;
}
void TCPServer::removeFromSessionList(TCPSession *session) {
std::vector<TCPSession *>::iterator cursor;
lock.lock();
for (cursor = sessions.begin(); cursor < sessions.end(); ++cursor)
if (*cursor == session)
{
sessions.erase(cursor);
break;
}
lock.unlock();
}
void TCPServer::sessionErrorHandler(std::string errorString, std::stringstream &out) {
throw coreutils::Exception(errorString);
}
TCPSession * TCPServer::getSocketAccept(EPoll &ePoll) {
return new TCPSession(ePoll, this);
}
void TCPServer::output(std::stringstream &out)
{
out << "Use the 'help' command to list the commands for this server." << std::endl;
}
int TCPServer::processCommand(coreutils::ZString &request, TCPSession &session)
{
// Was unguarded, like every other method below - `sessions` is
// mutated under `lock` by connectionRequest()/removeFromSessionList()
// (a session accepting or disconnecting on another thread), but
// every READ of it here had no lock at all. Confirmed live via
// dmesg: a real segfault at a stable, deterministic instruction
// offset (same crash signature as the Subscription::subscribers
// race fixed earlier this session, in SubscriptionManager.cpp) -
// Socket::write() -> resetSocket() dereferencing a null EPoll
// reference, consistent with iterating `sessions` while another
// thread concurrently erased and freed one of its entries.
// sendToAll() in particular is what most real gameplay broadcasts
// (combat, position, chat) go through, so this is very likely the
// actual dominant cause of that crash, not just the ItemDataBase
// subscriber list this session's earlier fix addressed.
std::lock_guard<std::mutex> guard(lock);
int sequence = 0;
for (auto *sessionx : sessions)
{
session.out << "|" << ++sequence;
sessionx->output(session.out);
session.out << "|" << std::endl;
}
return 1;
}
void TCPServer::sendToAll(std::stringstream &data) {
std::lock_guard<std::mutex> guard(lock);
for (auto session : sessions)
session->write(data.str());
data.str("");
}
void TCPServer::sendToAll(std::stringstream &data, TCPSession &sender) {
std::lock_guard<std::mutex> guard(lock);
for (auto session : sessions)
if (session != &sender)
session->write(data.str());
data.str("");
}
void TCPServer::sendToAll(std::stringstream &data, TCPSession &sender, SessionFilter filter) {
std::lock_guard<std::mutex> guard(lock);
for (auto session : sessions)
if (filter.test(*session))
if (session != &sender)
session->write(data.str());
data.str("");
}
TCPSession *TCPServer::getSessionByAlias(void *alias) {
coreutils::Log(coreutils::LOG_DEBUG_1) << alias;
std::lock_guard<std::mutex> guard(lock);
for (auto session : sessions)
if (session->alias == alias)
return session;
return NULL;
}
}