Skip to content

wsd: protect against thread-affinity violation in StreamSocket dtor#15120

Open
Ashod wants to merge 1 commit intomainfrom
private/ash/misc_affinity
Open

wsd: protect against thread-affinity violation in StreamSocket dtor#15120
Ashod wants to merge 1 commit intomainfrom
private/ash/misc_affinity

Conversation

@Ashod
Copy link
Contributor

@Ashod Ashod commented Mar 20, 2026

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

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

Call to function ensureDisconnected that calls virtual function
shutdownConnection
(overridden in
SslStreamSocket
).

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 calls StreamSocket::shutdownConnection() explicitly).
  • Modify ensureDisconnected() so it no longer calls asyncShutdown() and shutdownConnection(). That way, when it is invoked from the destructor we do not indirectly invoke any virtual function. We still maintain its primary contract: emitting onDisconnect() 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 narrow ensureDisconnected()’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 calls asyncShutdown(); and shutdownConnection(); 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.

Suggested changeset 1
net/Socket.hpp

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/net/Socket.hpp b/net/Socket.hpp
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -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.
             }
         }
 
EOF
@@ -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.
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@mmeeks mmeeks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-project-automation github-project-automation bot moved this from To Review to To Test in Collabora Online Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Test

Development

Successfully merging this pull request may close these issues.

2 participants