From 3b995e0e78459f11c90af7efea65856bf977390a Mon Sep 17 00:00:00 2001 From: "seer-by-sentry[bot]" <157164994+seer-by-sentry[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 20:49:35 +0000 Subject: [PATCH] bugfix(websocket): Prevent double-shutdown and ensure proper libcurl resource cleanup --- .../GeneralsOnline/OnlineServices_Init.cpp | 29 +++++++++++++++++-- .../OnlineServices_RoomsInterface.cpp | 20 +++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 3530de6fcd1..1fb2e112881 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1062,17 +1062,42 @@ std::string NGMP_OnlineServicesManager::GetPatcherDirectoryPath() void WebSocket::Shutdown() { + // Return immediately if already shut down to prevent double-shutdown + // (e.g., NGMP_OnlineServicesManager::Shutdown() calls this before releasing the shared_ptr, + // and then the shared_ptr destructor also calls Shutdown() via ~WebSocket()) + if (m_bShuttingDown) + { + return; + } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[WebSocket] Shutdown initiated"); // Signal that we're shutting down m_bShuttingDown = true; - // Disconnect from the websocket + // Disconnect from the websocket (handles the connected case) Disconnect(); - // Free headers + // Clean up curl easy handle if still active (e.g., mid-connection, not yet fully connected) + // Disconnect() returns early when m_bConnected is false, so m_pCurlWS may still be alive here + if (m_pCurlWS != nullptr) + { + if (m_pMulti != nullptr) + { + curl_multi_remove_handle(m_pMulti, m_pCurlWS); + } + curl_easy_cleanup(m_pCurlWS); + m_pCurlWS = nullptr; + } + // Clean up multi handle + if (m_pMulti != nullptr) + { + curl_multi_cleanup(m_pMulti); + m_pMulti = nullptr; + } + // Free headers (may already be freed by Disconnect, but check anyway) if (m_pHeaders != nullptr) { curl_slist_free_all(m_pHeaders); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 9cee01aee2b..5e0d906af5d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -17,6 +17,17 @@ WebSocket::WebSocket() WebSocket::~WebSocket() { Shutdown(); + // Only call Shutdown if it has not been initiated already. + // NGMP_OnlineServicesManager::Shutdown() calls Shutdown() before releasing the shared_ptr, + // so calling it again from the destructor would redundantly block for another 100ms sleep + // and attempt to free already-released curl resources. + if (!m_bShuttingDown) + { + Shutdown(); + } + + + if (m_pHeaders != nullptr) { @@ -196,6 +207,14 @@ void WebSocket::Disconnect() curl_slist_free_all(m_pHeaders); m_pHeaders = nullptr; } + + + // Remove from multi handle before cleanup (required by libcurl) + if (m_pMulti != nullptr) + { + curl_multi_remove_handle(m_pMulti, m_pCurlWS); + } + // cleanup curl_easy_cleanup(m_pCurlWS); @@ -1424,4 +1443,5 @@ void NGMP_OnlineServices_RoomsInterface::OnRosterUpdated(std::unordered_map