From 217ab9436c1ca1741da703aafd4d072178716fe7 Mon Sep 17 00:00:00 2001 From: Fabrice DIDIERJEAN Date: Mon, 20 Jul 2026 21:25:28 +0200 Subject: [PATCH 1/2] fix: don't read game user settings from the uWS thread (SIGSEGV) The uWS HTTP handlers read uWS.AuthenticationToken from config on every request. UFRMConfigManager::GetConfig reaches UFGGameUserSettings:: GetFGGameUserSettings(), which dereferences GEngine internally. That runs on the uWS worker thread, where nothing guarantees GEngine is still alive, and it faults during engine teardown: UEngine::GetGameUserSettings() <- SIGSEGV UFGGameUserSettings::GetFGGameUserSettings() UFRMConfigManager::GetConfig() <- AFicsitRemoteMonitoring::StartWebSocketServer lambda <- uWS::HttpRouter::executeHandlers Reproduced on a Linux dedicated server: poll any endpoint in a loop, then send SIGTERM. With the poll loop running the server died with SIGSEGV twice out of two; with the poll loop stopped, the same signal on the same binary exited cleanly (143). Two changes: - Cache the token. BeginPlay already reads it on the game thread; it now stores it in the existing AuthenticationToken member (previously assigned nowhere), and the three handlers read that member instead of hitting config per request. This also drops a redundant settings lookup from the hot path. - Guard GetConfig. The existing UserSettings null-check cannot help, because the fault happens inside the call that produces that pointer. Check GEngine first and return false so callers fall back to their default rather than crashing. This covers the other background-thread readers too, notably uWS.PushCycle in the push loop and Debug.JSONDebug during serialization. Also log the generated token rather than the local that is empty by definition on that branch. Co-Authored-By: Claude Opus 4.8 --- .../Private/FicsitRemoteMonitoring.cpp | 24 +++++++++++++------ .../Public/Libraries/FRMConfigManager.h | 11 +++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp index 6b0350a3..8f6df773 100644 --- a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp +++ b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp @@ -65,23 +65,33 @@ void AFicsitRemoteMonitoring::BeginPlay() // Load FRM's API Endpoints InitAPIRegistry(); + // Read the auth token ONCE here, on the game thread, and cache it in the + // AuthenticationToken member. The uWS request handlers must not read config + // per-request: that runs on the uWS worker thread and reaches GEngine, which + // is unsafe while the engine is tearing down (observed SIGSEGV). const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); - + // Debug log to verify token retrieval -Porisius // UE_LOGFMT(LogHttpServer, Log, "DEBUG: AuthToken - {AuthToken}", *AuthToken); - + if (AuthToken.IsEmpty()) { - if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GenerateAuthToken(32), false)) + const FString GeneratedToken = GenerateAuthToken(32); + if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GeneratedToken, false)) { UE_LOG(LogTemp, Warning, TEXT("Failed to apply setting")); return; } - UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *AuthToken); + AuthenticationToken = GeneratedToken; + + // Log the token that was actually generated. This previously logged + // AuthToken, which is empty on this branch by definition. + UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *GeneratedToken); } else { + AuthenticationToken = AuthToken; UE_LOG(LogTemp, Log, TEXT("Token already exists.")); } @@ -296,7 +306,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) // Log the request URL //UE_LOGFMT(LogHttpServer, Log, "Request URL: {0}", Endpoint); - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; FRequestData RequestData; RequestData.bIsAuthorized = IsAuthorizedRequest(req, AuthToken); @@ -330,7 +340,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) return UFRM_RequestLibrary::SendErrorMessage(res, "400 Bad Request", FString("Invalid Request Body")); } - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; FRequestData RequestData; RequestData.Method = "POST"; @@ -367,7 +377,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) std::string url(req->getUrl().begin(), req->getUrl().end()); - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; bool bFileExists = false; // Remove initial '/' diff --git a/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h b/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h index eb5ffe76..93d076a3 100644 --- a/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h +++ b/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h @@ -22,6 +22,17 @@ class FICSITREMOTEMONITORING_API UFRMConfigManager : public UBlueprintFunctionLi template static bool GetConfig(const FString& StrID, T& OutValue) { + // GetFGGameUserSettings() dereferences GEngine internally, so GEngine must + // be checked BEFORE the call — the UserSettings null-check below happens + // too late to help. During engine teardown GEngine is destroyed while the + // uWS worker thread may still be serving a request, and the dereference + // faults (SIGSEGV reading a small member offset). Degrade to the caller's + // default instead of crashing the server. + if (!GEngine) + { + return false; + } + UFGGameUserSettings* UserSettings = UFGGameUserSettings::GetFGGameUserSettings(); if (!UserSettings) { From dd868250bc3bf76121e0ec107f0591dacf47721a Mon Sep 17 00:00:00 2001 From: Fabrice DIDIERJEAN Date: Mon, 20 Jul 2026 21:46:35 +0200 Subject: [PATCH 2/2] fix: reject uWS requests once teardown has begun (SIGSEGV) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caching the auth token removed one faulting path, but the crash simply moved deeper — into endpoint execution itself: AFicsitRemoteMonitoring::CallEndpoint() FicsitRemoteMonitoring.cpp AFicsitRemoteMonitoring::HandleEndpoint() AFicsitRemoteMonitoring::HandleApiRequest() <- StartWebSocketServer lambda <- uWS::HttpRouter::executeHandlers The faulting instruction is CallEndpoint's own guard: if (!IsValid(WorldContext) || !IsValid(WorldContext->GetWorld())) That guard cannot work here. During teardown WorldContext is not null, it is freed, and IsValid() must dereference the object to read its flags — so the guard is reached and then faults. The same applies to `this`: the handler lambdas capture the subsystem actor, which is being destroyed too. Add a barrier that consults no UObject at all: - GFRMShuttingDown, a file-scope std::atomic. It must not be a member, because `this` may already be freed when a handler runs. - StopWebSocketServer() raises it first, before anything is torn down. That function is already wired to EndPlay and FCoreDelegates::OnExit. - StartWebSocketServer() lowers it, so a server restarted in the same session (`/frm http start` after a stop) does not answer 503 forever. - The four request handlers that reach the world check it first and answer 503 before touching the subsystem, the world, or any UObject. Verified on a Linux dedicated server with the documented repro — poll endpoints in a loop, then SIGTERM. Before: 3 crashes out of 3 attempts (exit 139). After: 3 clean shutdowns out of 3 (exit 143). Co-Authored-By: Claude Opus 4.8 --- .../Private/FicsitRemoteMonitoring.cpp | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp index 8f6df773..41de3313 100644 --- a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp +++ b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp @@ -1,5 +1,6 @@ #include "FicsitRemoteMonitoring.h" +#include #include #include "Runtime/Core/Public/Logging/LogCategory.h" @@ -37,6 +38,36 @@ us_listen_socket_t* SocketListener; bool SocketRunning = false; +// Set as soon as teardown begins, cleared when a server starts. Deliberately a +// file-scope atomic rather than a member: once teardown starts, the subsystem +// actor and the world are being destroyed, so the uWS worker thread cannot +// safely touch `this`, the World, or any UObject — not even to call IsValid(), +// which must dereference the object to read its flags. A dangling pointer is +// not null, so the IsValid() guards inside CallEndpoint() are reached and then +// fault. This flag is the only state a request handler can consult without +// dereferencing anything that may already be freed. +std::atomic GFRMShuttingDown{false}; + +// Shutdown barrier for uWS request handlers. Answers 503 and reports true when +// teardown has begun, so the caller returns before touching the subsystem, the +// world, or any other UObject. Touches nothing but the response socket, which +// uWS owns and keeps alive for the duration of the callback. +static bool FRMRejectIfShuttingDown(uWS::HttpResponse* Res) +{ + if (!GFRMShuttingDown.load(std::memory_order_acquire)) + { + return false; + } + + if (Res) + { + Res->writeStatus("503 Service Unavailable"); + Res->end("{\"error\":\"Server is shutting down\"}"); + } + + return true; +} + AFicsitRemoteMonitoring* AFicsitRemoteMonitoring::Get(UWorld* WorldContext) { for (TActorIterator It(WorldContext, StaticClass(), EActorIteratorFlags::AllActors); It; ++It) { @@ -138,6 +169,11 @@ void AFicsitRemoteMonitoring::EndPlay(const EEndPlayReason::Type EndPlayReason) void AFicsitRemoteMonitoring::StopWebSocketServer() { + // Raise the shutdown barrier FIRST, before anything is torn down, so any + // request already inside the uWS worker thread bails out with 503 instead + // of walking into a half-destroyed world. + GFRMShuttingDown.store(true, std::memory_order_release); + bShouldStop = true; // Signal the WebSocket server to stop @@ -178,10 +214,15 @@ FArduinoConfig AFicsitRemoteMonitoring::GetSerialConfig() return Config; } -void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) +void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) { UE_LOGFMT(LogHttpServer, Log, "Initializing WebSocket Service"); + // Lower the shutdown barrier: a previous StopWebSocketServer() raised it, and + // without this a server restarted in the same session (e.g. `/frm http start` + // after a stop) would answer 503 to every request forever. + GFRMShuttingDown.store(false, std::memory_order_release); + if (SocketRunning) { if (bSkipIfRunning) @@ -300,6 +341,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) }); app.get("/api/:APIEndpoint", [this, World](auto* res, auto* req) { + if (FRMRejectIfShuttingDown(res)) return; std::string url(req->getParameter("APIEndpoint")); FString Endpoint = FString(url.c_str()); @@ -323,11 +365,13 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) app.post("/*", [this, World](auto* res, uWS::HttpRequest* req) { + if (FRMRejectIfShuttingDown(res)) return; const std::string URL(req->getUrl().begin(), req->getUrl().end()); FString RelativePath = FString(URL.c_str()).Mid(1); res->onData([this, res, req, World, RelativePath](const std::string_view data, bool) { + if (FRMRejectIfShuttingDown(res)) return; try { const std::string PostData(data); @@ -373,6 +417,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) }); app.get("/*", [this, UIPath, World](auto* res, uWS::HttpRequest* req) { + if (FRMRejectIfShuttingDown(res)) return; if (!res) return; std::string url(req->getUrl().begin(), req->getUrl().end());