Skip to content

Sockets (Qt): fix send/close race that hangs send() forever - #1842

Merged
Mysticial merged 1 commit into
mainfrom
qt-socket-close-send-race-fix
Nov 4, 2026
Merged

Mysticial merged 1 commit into
mainfrom
qt-socket-close-send-race-fix

Conversation

@pa-devbot

Copy link
Copy Markdown
Collaborator

What

Fixes the intermittent CI test-runner hang (job sits until the 8h timeout, zero log progress): the parallel test runner waits for every test to complete, and this test can never complete.

ClientSocket_Qt::send() copied the payload (up to 256 MiB in the socket
tests) before registering the send in m_inflight_sends. If close() - or a
reconnecting connect() - ran during that window, it cancelled an empty
list and queued the socket's destruction; the send's queued
internal_send call was then posted after the destruction event, Qt
silently discarded it, and "done" was never set. The infinite-timeout
wait in send() then blocked forever, the dispatched send task never
finished, and the whole suite hung.

(ServerSocket_Qt::send documents this exact Qt limitation and bounds its
wait for this reason; the client relies on close() cancelling the send,
which this race defeats.)

Registers the send under m_lock while re-checking that m_socket is still
attached: a send either registers before close() moves the list (and is
cancelled, waking the wait) or sees the detached socket and returns 0
without queueing anything. Either way, the unbounded infinite-timeout
wait is actually always ended, as its comment claims.

How this was found

A hang watchdog for the test runner (#1840) dumped the stuck state in
CI: Running for 223s: Sockets (Qt) - close() cancels an in-flight send, with the send task parked in cv.wait inside
ClientSocket_Qt::send (both Qt event threads idle).

Verification

  • Deterministic repro of the exact ordering (256 MiB send, close()
    during the payload copy): hung 100% before the fix, passes after
    (25+ iterations, close at 0/15/100 ms).
  • Full suite with the fix: local (aarch64, Qt 6.10.2) completes clean;
    CI green on all 6 platforms, e.g. Windows: 26,522 passed / 0 failed,
    including all Sockets (Qt) and Sockets (WinSocket) tests.

Follows #1831 (close() cancels in-flight sends) and #1838 (runner on
the uncapped pool). Related: #1840 (the hang watchdog used to
diagnose this).

ClientSocket_Qt::send() copied the payload (up to 256 MiB in the socket
tests) before registering the send in m_inflight_sends. If close() - or a
reconnecting connect() - ran during that window, it cancelled an empty
list and queued the socket's destruction; the send's queued
internal_send call was then posted after the destruction event, Qt
silently discarded it, and "done" was never set. The infinite-timeout
wait in send() then blocked forever, the dispatched send task never
finished, and the whole test suite hung - the ParallelUnitTestRunner
waits for every test to complete. (ServerSocket_Qt::send documents this
exact Qt limitation and bounds its wait for this reason; the client
relies on close() cancelling the send, which this race defeats.)

Register the send under m_lock while re-checking that m_socket is still
attached: a send either registers before close() moves the list (and is
cancelled, waking the wait) or sees the detached socket and returns 0
without queueing anything. Either way, the unbounded infinite-timeout
wait is actually always ended, as its comment claims.

Reproduced deterministically before the fix: 256 MiB send with close()
during the payload copy hung the send task forever (close() returned in
1 ms, both event threads idle, caller parked in cv.wait). Passes after.
@Mysticial
Mysticial merged commit 1ff8a0b into main Nov 4, 2026
7 checks passed
@Mysticial
Mysticial deleted the qt-socket-close-send-race-fix branch November 4, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants