diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index 8c96db5f32f..6168a80c1c3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -53,6 +53,7 @@ class AnticheatPlugInterface static void BeginSession(); static void EndSession(); + static void RegisterHostCallbacks(); // Callbacks from plugin typedef void (*LoginCallback)(bool bSuccess); @@ -84,6 +85,7 @@ class AnticheatPlugInterface typedef void (*FuncDefTick)(void); typedef void (*FuncDefShutdown)(void); + typedef void (*FuncDefClearAllHostCallbacks)(void); struct AnticheatPluginFunctionPtrs { @@ -105,6 +107,7 @@ class AnticheatPlugInterface FuncDefDeregisterPlayer fnDeregisterPlayer = nullptr; FuncDefTick fnTick = nullptr; FuncDefShutdown fnShutdown = nullptr; + FuncDefClearAllHostCallbacks fnClearAllHostCallbacks = nullptr; }; static AnticheatPluginFunctionPtrs Functions; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 0cd3df16864..e0035cd5fca 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -3,6 +3,7 @@ #include "../NetworkMesh.h" #include "../OnlineServices_Init.h" #include "../OnlineServices_Auth.h" +#include #define AC_PLUGIN_LOAD_FUNCTION(funcName) \ AnticheatPlugInterface::Functions.fn##funcName = (FuncDef##funcName)GetProcAddress(g_hACPluginModule, #funcName); \ @@ -14,6 +15,12 @@ return; \ } +// Tracks whether an anti-cheat session is currently active. The Set* callbacks +// registered below are invoked asynchronously by EOS; once EndSession() clears +// this flag (which LeaveCurrentLobby() does before deleting the lobby mesh), +// they must not dereference lobby/mesh objects that are about to be freed. +static std::atomic g_bACSessionActive{ false }; + bool AnticheatPlugInterface::IsExternalProcessRunning() { if (IsPluginLoaded() && Functions.fnIsExternalProcessRunning != nullptr) @@ -61,11 +68,6 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) // set logger AC_PLUGIN_LOAD_FUNCTION(SetLoggingFunction); - Functions.fnSetLoggingFunction([](const char* szMsg) - { - //MessageBoxA(nullptr, szMsg, szMsg, MB_OK); - NetworkLog(ELogVerbosity::LOG_RELEASE, szMsg); - }); // Initialize AC AC_PLUGIN_LOAD_FUNCTION(Initialize); @@ -88,90 +90,148 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) // integrity callback AC_PLUGIN_LOAD_FUNCTION(SetACIntegrityViolationOccurredCallback); - Functions.fnSetACIntegrityViolationOccurredCallback([](const char* szReason, int violationType) - { - if (szReason == nullptr) - { - szReason = "(null reason)"; - } - - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, local AC integrity violation occured (%d): %s.", violationType, szReason); - g_bPendingExitLobby = true; - }); // set action required callback AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); - Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) + + // set transport callback + AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); + + // AC network message arrived callback + AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); + + // Login funcs + AC_PLUGIN_LOAD_FUNCTION(Login); + AC_PLUGIN_LOAD_FUNCTION(RefreshToken); + AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); + AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); + + // Begin and end session funcs + AC_PLUGIN_LOAD_FUNCTION(BeginSession); + AC_PLUGIN_LOAD_FUNCTION(EndSession); + + // register player funcs + AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); + AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); + + AC_PLUGIN_LOAD_FUNCTION(Tick); + AC_PLUGIN_LOAD_FUNCTION(Shutdown); + + // Optional: callback de-registration (added in AntiCheatPlugin PR #2). + // Resolved WITHOUT the fail-hard macro so older plugin DLLs still load. + Functions.fnClearAllHostCallbacks = + (FuncDefClearAllHostCallbacks)GetProcAddress(g_hACPluginModule, "ClearAllHostCallbacks"); + + // Install the host callbacks for the first time. EndSession() clears them + // before the lobby mesh is freed; BeginSession() re-installs them. + RegisterHostCallbacks(); + } +} + +void AnticheatPlugInterface::RegisterHostCallbacks() +{ + // (Re)install every host callback the plugin may invoke. The action-required + // and transport callbacks dereference the per-lobby NetworkMesh, so EndSession() + // clears them all (ClearAllHostCallbacks) before that mesh is freed; they must + // therefore be re-registered whenever a session starts. + if (!IsPluginLoaded()) + { + return; + } + + Functions.fnSetLoggingFunction([](const char* szMsg) + { + //MessageBoxA(nullptr, szMsg, szMsg, MB_OK); + NetworkLog(ELogVerbosity::LOG_RELEASE, szMsg); + }); + + Functions.fnSetACIntegrityViolationOccurredCallback([](const char* szReason, int violationType) + { + if (szReason == nullptr) + { + szReason = "(null reason)"; + } + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, local AC integrity violation occured (%d): %s.", violationType, szReason); + g_bPendingExitLobby = true; + }); + + Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) + { + // Session torn down - the mesh this may act on can be freed. Bail. + if (!g_bACSessionActive) { - if (szReason == nullptr) - { - szReason = "(null reason)"; - } + return; + } + + if (szReason == nullptr) + { + szReason = "(null reason)"; + } + + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); - NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required: %s", szReason); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required: %s", szReason); + if (pAuthInterface == nullptr) + { + // no auth interface? bail out + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, no auth interface."); + g_bPendingExitLobby = true; + return; + } + + // If it's us, leave, if its someone else, d/c them + uint32_t localUserID = pAuthInterface->GetUserID(); + if (localUserID == userID) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, action was requested against local user."); + g_bPendingExitLobby = true; + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnecting remote user, lobby isn't secure, action was requested against remote user %u.", userID); - if (pAuthInterface == nullptr) + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) { - // no auth interface? bail out - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, no auth interface."); - g_bPendingExitLobby = true; - return; + pMesh->DisconnectUser(userID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnected: %u.", userID); } - - // If it's us, leave, if its someone else, d/c them - uint32_t localUserID = pAuthInterface->GetUserID(); - if (localUserID == userID) + else // no mesh, just back out { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, action was requested against local user."); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, actionable player was remote, but no mesh exists to take action."); g_bPendingExitLobby = true; } - else - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnecting remote user, lobby isn't secure, action was requested against remote user %u.", userID); + } + }); - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); - if (pMesh != nullptr) - { - pMesh->DisconnectUser(userID); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnected: %u.", userID); - } - else // no mesh, just back out - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, actionable player was remote, but no mesh exists to take action."); - g_bPendingExitLobby = true; - } - } - }); + Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) + { + // Session torn down (e.g. leaving a lobby) - the mesh may be freed. Bail. + if (!g_bACSessionActive) + { + return; + } - // set transport callback - AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); - Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) + if (pData == nullptr || dataLen == 0) { - if (pData == nullptr || dataLen == 0) - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: SendMessageViaTransport received null/empty data"); - return; - } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: SendMessageViaTransport received null/empty data"); + return; + } - // prefer websocket if we have it, otherwise fall back to p2p mesh - bool bFallbackToP2P = false; - std::shared_ptr pWS = NGMP_OnlineServicesManager::GetWebSocket(); - if (pWS != nullptr) + // prefer websocket if we have it, otherwise fall back to p2p mesh + bool bFallbackToP2P = false; + std::shared_ptr pWS = NGMP_OnlineServicesManager::GetWebSocket(); + if (pWS != nullptr) + { + if (pWS->IsConnected()) { - if (pWS->IsConnected()) + if (dataLen > 0) { - if (dataLen > 0) - { - std::vector vecPayload((uint8_t*)pData, (uint8_t*)pData + dataLen); - pWS->SendData_ACMessage(goUserID, vecPayload); - } - else - { - bFallbackToP2P = true; - } + std::vector vecPayload((uint8_t*)pData, (uint8_t*)pData + dataLen); + pWS->SendData_ACMessage(goUserID, vecPayload); } else { @@ -182,42 +242,26 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) { bFallbackToP2P = true; } + } + else + { + bFallbackToP2P = true; + } - if (bFallbackToP2P) + if (bFallbackToP2P) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AC Packets - WebSocket unavailable, falling back to P2P"); + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AC Packets - WebSocket unavailable, falling back to P2P"); - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); - if (pMesh != nullptr) - { - pMesh->SendACPacket(goUserID, pData, dataLen); - } - else - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot send AC packet - NetworkMesh is null"); - } + pMesh->SendACPacket(goUserID, pData, dataLen); } - }); - - // AC network message arrived callback - AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); - - // Login funcs - AC_PLUGIN_LOAD_FUNCTION(Login); - AC_PLUGIN_LOAD_FUNCTION(RefreshToken); - AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); - AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); - - // Begin and end session funcs - AC_PLUGIN_LOAD_FUNCTION(BeginSession); - AC_PLUGIN_LOAD_FUNCTION(EndSession); - - // register player funcs - AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); - AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); - - AC_PLUGIN_LOAD_FUNCTION(Tick); - AC_PLUGIN_LOAD_FUNCTION(Shutdown); - } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot send AC packet - NetworkMesh is null"); + } + } + }); } bool AnticheatPlugInterface::g_bPendingExitLobby = false; @@ -294,6 +338,18 @@ bool g_bSessionStarted = false; void AnticheatPlugInterface::BeginSession() { + if (g_bSessionStarted) + { + // Already active - a duplicate BeginSession() is rejected by the plugin + // (EOS_AlreadyConfigured) and is part of the begin/end churn. + return; + } + + // Re-install the host callbacks for this session: EndSession() de-registered + // them so the plugin could not call into a freed mesh, so they must be set + // again before the session (and any callbacks) start. + RegisterHostCallbacks(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] BeginSession() called"); NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] IsPluginLoaded=%d, fnBeginSession=%p", IsPluginLoaded(), Functions.fnBeginSession); @@ -302,6 +358,7 @@ void AnticheatPlugInterface::BeginSession() NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Calling plugin fnBeginSession()"); Functions.fnBeginSession(); g_bSessionStarted = true; + g_bACSessionActive = true; NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Plugin fnBeginSession() completed"); } else @@ -312,11 +369,32 @@ void AnticheatPlugInterface::BeginSession() void AnticheatPlugInterface::EndSession() { + if (!g_bSessionStarted) + { + // No active session - calling EndSession() now is rejected by the + // plugin (EOS_NotConfigured) and is part of the begin/end churn. + return; + } + + // Stop our async callbacks from touching the mesh BEFORE it is torn down. + g_bACSessionActive = false; + if (IsPluginLoaded() && Functions.fnEndSession != nullptr) { Functions.fnEndSession(); - g_bSessionStarted = false; } + + // De-register the host callbacks under the plugin state lock. This blocks + // until any in-flight worker-thread callback returns and nulls the stored + // pointers, so the lobby mesh (freed right after EndSession() in + // LeaveCurrentLobby) can no longer be dereferenced by a late callback. + // No-ops against older plugin DLLs that do not export it yet. + if (Functions.fnClearAllHostCallbacks != nullptr) + { + Functions.fnClearAllHostCallbacks(); + } + + g_bSessionStarted = false; } AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Functions; @@ -411,6 +489,13 @@ void AnticheatPlugInterface::UnloadPlugin() { if (IsPluginLoaded()) { + // Stop async callbacks and de-register host callbacks before teardown. + g_bACSessionActive = false; + if (Functions.fnClearAllHostCallbacks != nullptr) + { + Functions.fnClearAllHostCallbacks(); + } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Starting Shutdown"); if (Functions.fnShutdown != nullptr) { diff --git a/eac_uaf_clientfix.patch b/eac_uaf_clientfix.patch new file mode 100644 index 00000000000..e5e156114d0 --- /dev/null +++ b/eac_uaf_clientfix.patch @@ -0,0 +1,142 @@ +diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +index 8c96db5..652f3fd 100644 +--- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h ++++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +@@ -84,6 +84,7 @@ public: + typedef void (*FuncDefTick)(void); + + typedef void (*FuncDefShutdown)(void); ++ typedef void (*FuncDefClearAllHostCallbacks)(void); + + struct AnticheatPluginFunctionPtrs + { +@@ -105,6 +106,7 @@ public: + FuncDefDeregisterPlayer fnDeregisterPlayer = nullptr; + FuncDefTick fnTick = nullptr; + FuncDefShutdown fnShutdown = nullptr; ++ FuncDefClearAllHostCallbacks fnClearAllHostCallbacks = nullptr; + }; + static AnticheatPluginFunctionPtrs Functions; + +diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +index 0cd3df1..b12dcbc 100644 +--- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp ++++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +@@ -3,6 +3,7 @@ + #include "../NetworkMesh.h" + #include "../OnlineServices_Init.h" + #include "../OnlineServices_Auth.h" ++#include + + #define AC_PLUGIN_LOAD_FUNCTION(funcName) \ + AnticheatPlugInterface::Functions.fn##funcName = (FuncDef##funcName)GetProcAddress(g_hACPluginModule, #funcName); \ +@@ -14,6 +15,12 @@ + return; \ + } + ++// Tracks whether an anti-cheat session is currently active. The Set* callbacks ++// registered below are invoked asynchronously by EOS; once EndSession() clears ++// this flag (which LeaveCurrentLobby() does before deleting the lobby mesh), ++// they must not dereference lobby/mesh objects that are about to be freed. ++static std::atomic g_bACSessionActive{ false }; ++ + bool AnticheatPlugInterface::IsExternalProcessRunning() + { + if (IsPluginLoaded() && Functions.fnIsExternalProcessRunning != nullptr) +@@ -104,6 +111,12 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) + + Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) + { ++ // Session torn down - the mesh this may act on can be freed. Bail. ++ if (!g_bACSessionActive) ++ { ++ return; ++ } ++ + if (szReason == nullptr) + { + szReason = "(null reason)"; +@@ -150,6 +163,12 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) + AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); + Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) + { ++ // Session torn down (e.g. leaving a lobby) - the mesh may be freed. Bail. ++ if (!g_bACSessionActive) ++ { ++ return; ++ } ++ + if (pData == nullptr || dataLen == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: SendMessageViaTransport received null/empty data"); +@@ -217,6 +236,11 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) + + AC_PLUGIN_LOAD_FUNCTION(Tick); + AC_PLUGIN_LOAD_FUNCTION(Shutdown); ++ ++ // Optional: callback de-registration (added in AntiCheatPlugin PR #2). ++ // Resolved WITHOUT the fail-hard macro so older plugin DLLs still load. ++ Functions.fnClearAllHostCallbacks = ++ (FuncDefClearAllHostCallbacks)GetProcAddress(g_hACPluginModule, "ClearAllHostCallbacks"); + } + } + +@@ -294,6 +318,13 @@ bool g_bSessionStarted = false; + + void AnticheatPlugInterface::BeginSession() + { ++ if (g_bSessionStarted) ++ { ++ // Already active - a duplicate BeginSession() is rejected by the plugin ++ // (EOS_AlreadyConfigured) and is part of the begin/end churn. ++ return; ++ } ++ + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] BeginSession() called"); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] IsPluginLoaded=%d, fnBeginSession=%p", IsPluginLoaded(), Functions.fnBeginSession); + +@@ -302,6 +333,7 @@ void AnticheatPlugInterface::BeginSession() + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Calling plugin fnBeginSession()"); + Functions.fnBeginSession(); + g_bSessionStarted = true; ++ g_bACSessionActive = true; + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Plugin fnBeginSession() completed"); + } + else +@@ -312,11 +344,21 @@ void AnticheatPlugInterface::BeginSession() + + void AnticheatPlugInterface::EndSession() + { ++ if (!g_bSessionStarted) ++ { ++ // No active session - calling EndSession() now is rejected by the ++ // plugin (EOS_NotConfigured) and is part of the begin/end churn. ++ return; ++ } ++ ++ // Stop our async callbacks from touching the mesh BEFORE it is torn down. ++ g_bACSessionActive = false; ++ + if (IsPluginLoaded() && Functions.fnEndSession != nullptr) + { + Functions.fnEndSession(); +- g_bSessionStarted = false; + } ++ g_bSessionStarted = false; + } + + AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Functions; +@@ -411,6 +453,13 @@ void AnticheatPlugInterface::UnloadPlugin() + { + if (IsPluginLoaded()) + { ++ // Stop async callbacks and de-register host callbacks before teardown. ++ g_bACSessionActive = false; ++ if (Functions.fnClearAllHostCallbacks != nullptr) ++ { ++ Functions.fnClearAllHostCallbacks(); ++ } ++ + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Starting Shutdown"); + if (Functions.fnShutdown != nullptr) + {