Skip to content

net: ~SocketBackend takes _socketMtx across shutdownBoth(), the deadlock SocketServer::close() documents avoiding #506

Description

@Yaraslaut

Split out of #498, which bundled it with an unrelated resource leak. Filed separately because the fix, the file and the severity are all different.

Summary

~SocketBackend acquires _socketMtx before calling shutdownBoth(). sendFrame holds that same mutex across a blocking, un-timed sendAll. Against a stalled peer the destructor blocks on the very lock whose holder only shutdownBoth() would release.

Verification status

Inferred from reading the code; not reproduced. I did not construct a stalled-peer teardown. Revision: origin/master adfe8e5f plus doc-only commits.

socket_backend.hpp:89-93:

std::scoped_lock lock{_socketMtx};
if (_socket.valid()) { _socket.shutdownBoth(); }

sendFrame (:380-387) takes _socketMtx and then calls _socket.sendAll(frame.data(), frame.size()); sendAll (tcp_socket.hpp:327-339) loops on a blocking ::send with no timeout.

Why this is a defect and not a design choice

The sibling class states the trap and deliberately does the opposite. SocketServer::close() (socket_server.hpp:174-183):

"Taking it here would deadlock exactly when the shutdown is most needed: a client thread blocked in sendAll against a stalled peer's full socket buffer holds writeMtx for as long as that send is stuck, and close() has no timeout — it is reached from the destructor. shutdownBoth() is documented safe from any thread and is itself the mechanism that unblocks that send … Locking to 'protect' the socket would therefore wait on the very thing it is trying to interrupt."

Every clause applies to ~SocketBackend. shutdownBoth() is documented safe from any thread (tcp_socket.hpp:341-342), so the lock buys nothing that the I/O thread's ownership of _socket's writes does not already give.

The I/O thread's own Pong/Close echo in drainFrames (socket_backend.hpp:583, :589) reaches sendFrame too, so the holder need not even be an application thread.

What would change the verdict

  • Close it if sendFrame is shown unreachable concurrently with ~SocketBackend.
  • Raise it by reproducing: a peer that accepts and never reads, filling the send buffer, then destroy the backend.

A regression test must actually stall the peer — a test that destroys an idle backend would pass with or without the fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreSubsystem: corebugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions