wsd: protect against thread-affinity violation in StreamSocket dtor#15120
wsd: protect against thread-affinity violation in StreamSocket dtor#15120
Conversation
As the comment explains, there are scenarios when the socket is closed and removed while it's held by a temporary shared_ptr acquired through a weak_ptr in another thread. In such a scenario, the temp shared_ptr will call the destructor in its incorrect thread, causing the thread-affinity assertion to fire inside of ensureDisconnected(). This isn't helpful for the main reason that the ensureDisconnected() isn't necessary and will not do anything but check that there is nothing to be done! And since avoiding the destruction in a different thread than the one we have affinity to is rather complex and tricky, we avoid the issue by skipping the ensureDisconnected() call when it's not needed. Another fix would've been to reset the thread affinity, but that seems to be too blunt a tool and robs us of the opportunity of catching affinity violations when ensureDisconnected() isn't invoked on destruction while we are destroyed from the wrong thread. Change-Id: I8e41ee56a4203aaba3d21472c9d1a446cb76f9a2 Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
net/Socket.hpp
Dismissed
| // tripped-up by the ASSERT_CORRECT_SOCKET_THREAD check inside it. | ||
| // Otherwise, we will invoke it and it's only fair to catch the thread | ||
| // affinity violation. | ||
| ensureDisconnected(); |
Check warning
Code scanning / CodeQL
Virtual call from constructor or destructor Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 14 hours ago
In general, the fix is to ensure that constructors and destructors of base classes do not cause virtual dispatch, directly or indirectly. Here, the destructor of StreamSocket calls ensureDisconnected(), which in turn calls the virtual shutdownConnection(). We should refactor so that ensureDisconnected() performs only non-virtual logic and leaves the actual shutdown call to a context where virtual dispatch is appropriate, or to an explicit base-class call.
The simplest change without altering external behavior is:
- Keep
StreamSocket::~StreamSocket()as-is in terms of visible effects (it still performs disconnect logic and then callsStreamSocket::shutdownConnection()explicitly). - Modify
ensureDisconnected()so it no longer callsasyncShutdown()andshutdownConnection(). That way, when it is invoked from the destructor we do not indirectly invoke any virtual function. We still maintain its primary contract: emittingonDisconnect()once and logging if the socket remains open. - Shutdown during destruction continues to be handled exclusively by the explicit call to
StreamSocket::shutdownConnection()in the destructor (StreamSocket::shutdownConnection();), which is a non-virtual qualified call and therefore safe in a destructor. - In non-destruction contexts, callers that require both disconnect notification and shutdown can perform
ensureDisconnected(); asyncShutdown(); shutdownConnection();themselves if needed. Since we cannot see other callers in this snippet, we avoid modifying them; we only narrowensureDisconnected()’s responsibilities, which is less likely to break logic because the destructor already performs shutdown unconditionally.
Concretely, in net/Socket.hpp:
- In the body of
ensureDisconnected(), remove the block that conditionally callsasyncShutdown();andshutdownConnection();when!isShutdown(). Leave the rest of the method unchanged.
This eliminates the virtual call from the destructor while preserving existing destruction semantics: onDisconnect() remains called once, and shutdown is still performed via the explicit base call in the destructor.
| @@ -1278,11 +1278,7 @@ | ||
| // The SocketHandler has a weak pointer to us and we could | ||
| // be getting destroyed at this point, so it won't get a | ||
| // reference to us from the weak pointer, and so can't disconnect. | ||
| if (!isShutdown()) | ||
| { | ||
| asyncShutdown(); // signal | ||
| shutdownConnection(); // real -> setShutdown() | ||
| } | ||
| // Any required shutdown should be performed explicitly by the caller. | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Ooh - I worry about this case I guess; are we sure that all of this cleanup happens properly in the other threads ? eg. ChildSession::disconnect called from ~ChildSession - from ~ProtocolHandlerInterface looks problematic as an example. I assume we are still warning of thread affinity in that chain though (?) =)
Anyhow - it seems quite plausible, and simple enough. Not sure we want a novel in the comment there though - should be visible from the commit message.
mmeeks
left a comment
There was a problem hiding this comment.
Fine - but can we comment less verbosely there and leave it to the commit msg :-) or better a cool# ticket reference ?
net/Socket.hpp
Dismissed
| // tripped-up by the ASSERT_CORRECT_SOCKET_THREAD check inside it. | ||
| // Otherwise, we will invoke it and it's only fair to catch the thread | ||
| // affinity violation. | ||
| ensureDisconnected(); |
There was a problem hiding this comment.
Ooh - I worry about this case I guess; are we sure that all of this cleanup happens properly in the other threads ? eg. ChildSession::disconnect called from ~ChildSession - from ~ProtocolHandlerInterface looks problematic as an example. I assume we are still warning of thread affinity in that chain though (?) =)
Anyhow - it seems quite plausible, and simple enough. Not sure we want a novel in the comment there though - should be visible from the commit message.
As the comment explains, there are scenarios
when the socket is closed and removed while
it's held by a temporary shared_ptr acquired
through a weak_ptr in another thread. In such
a scenario, the temp shared_ptr will call the
destructor in its incorrect thread, causing
the thread-affinity assertion to fire inside
of ensureDisconnected().
This isn't helpful for the main reason that
the ensureDisconnected() isn't necessary and
will not do anything but check that there is
nothing to be done! And since avoiding the
destruction in a different thread than the
one we have affinity to is rather complex
and tricky, we avoid the issue by skipping
the ensureDisconnected() call when it's not
needed.
Another fix would've been to reset the thread
affinity, but that seems to be too blunt a
tool and robs us of the opportunity of catching
affinity violations when ensureDisconnected()
isn't invoked on destruction while we are
destroyed from the wrong thread.
Change-Id: I8e41ee56a4203aaba3d21472c9d1a446cb76f9a2
Signed-off-by: Ashod Nakashian ashod.nakashian@collabora.co.uk