From edf6871f5264fd360faa8292ccb2565f9032f200 Mon Sep 17 00:00:00 2001 From: Lukas Marek Date: Thu, 18 Jun 2026 21:50:55 +0200 Subject: [PATCH] Fix use-after-free of FSocketIONative from its own game-thread callbacks SetupInternalCallbacks() marshals each internal listener (connect/disconnect/ namespace/fail/reconnect) onto the game thread via RunShortLambdaOnGameThread, and those lambdas captured the FSocketIONative by reference. The deferred task lives on the game-thread task graph independently of the object, so if the native client is released before the task runs, the lambda dereferences freed memory. This reproduces during a UE level transition: tearing down the old world releases the client on a background thread, and LoadMap's own FlushRenderingCommands then pumps the game-thread task graph and runs the stale callback. Observed as an access violation reading 0xffffffffffffffff on the game thread, top frame being the namespace-disconnect lambda. FSocketIONative now derives from TSharedFromThis and each deferred lambda captures a TWeakPtr and Pin()s it, so it no-ops if the object is already gone. A weak (not strong) capture avoids extending the object's lifetime and moving its destruction (which joins the network thread) onto the game thread. This is the same class of fix as the component-level weak-pointer guard, one layer down at the native object. The refcount has to be atomic, since the object can be released on a background thread while the lambda Pin()s it on the game thread; on UE5 that needs no explicit mode, as TSharedPtr/TWeakPtr/TSharedFromThis already default to ESPMode::ThreadSafe. The outer asio-thread listeners intentionally keep their [&] capture: PrivateClient is the last member, so ~FSocketIONative destroys it first and the network thread is joined before the other members are torn down. --- .../SocketIOClient/Private/SocketIONative.cpp | 63 +++++++++++++------ Source/SocketIOClient/Public/SocketIONative.h | 2 +- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/Source/SocketIOClient/Private/SocketIONative.cpp b/Source/SocketIOClient/Private/SocketIONative.cpp index 3dcffb5..9235b29 100644 --- a/Source/SocketIOClient/Private/SocketIONative.cpp +++ b/Source/SocketIOClient/Private/SocketIONative.cpp @@ -501,11 +501,18 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&, DisconnectReason] + // Capture a weak ref to self instead of `this`: this lambda runs later on the + // game thread, by which point the FSocketIONative may have been released (e.g. + // during a level transition). Pin() safely no-ops if it's gone. + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf, DisconnectReason] { - if (OnDisconnectedCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnDisconnectedCallback(DisconnectReason); + if (Self->OnDisconnectedCallback) + { + Self->OnDisconnectedCallback(DisconnectReason); + } } }); } @@ -537,11 +544,15 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&] + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf] { - if (OnConnectedCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnConnectedCallback(SocketId, SessionId); + if (Self->OnConnectedCallback) + { + Self->OnConnectedCallback(Self->SocketId, Self->SessionId); + } } }); } @@ -562,11 +573,15 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&, Namespace] + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf, Namespace] { - if (OnNamespaceConnectedCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnNamespaceConnectedCallback(Namespace); + if (Self->OnNamespaceConnectedCallback) + { + Self->OnNamespaceConnectedCallback(Namespace); + } } }); } @@ -593,11 +608,15 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&, Namespace] + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf, Namespace] { - if (OnNamespaceDisconnectedCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnNamespaceDisconnectedCallback(Namespace); + if (Self->OnNamespaceDisconnectedCallback) + { + Self->OnNamespaceDisconnectedCallback(Namespace); + } } }); } @@ -618,11 +637,15 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&] + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf] { - if (OnFailCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnFailCallback(); + if (Self->OnFailCallback) + { + Self->OnFailCallback(); + } } }); } @@ -645,11 +668,15 @@ void FSocketIONative::SetupInternalCallbacks() { if (bCallbackOnGameThread) { - FCULambdaRunnable::RunShortLambdaOnGameThread([&, num, delay] + TWeakPtr WeakSelf = AsShared(); + FCULambdaRunnable::RunShortLambdaOnGameThread([WeakSelf, num, delay] { - if (OnReconnectionCallback) + if (TSharedPtr Self = WeakSelf.Pin()) { - OnReconnectionCallback(num, delay); + if (Self->OnReconnectionCallback) + { + Self->OnReconnectionCallback(num, delay); + } } }); } diff --git a/Source/SocketIOClient/Public/SocketIONative.h b/Source/SocketIOClient/Public/SocketIONative.h index d198fd1..a1bd9ab 100644 --- a/Source/SocketIOClient/Public/SocketIONative.h +++ b/Source/SocketIOClient/Public/SocketIONative.h @@ -39,7 +39,7 @@ struct FSIOBoundEvent } }; -class SOCKETIOCLIENT_API FSocketIONative +class SOCKETIOCLIENT_API FSocketIONative : public TSharedFromThis { public: /** By default TLS verification is off. TLS mode will be set by URL on connect.*/