Skip to content

Fix aclose_forcefully() returning before the socket is released - #1278

Closed
Sohel2309 wants to merge 1 commit into
agronholm:masterfrom
Sohel2309:fix-1273-concurrent-aclose-forcefully
Closed

Fix aclose_forcefully() returning before the socket is released#1278
Sohel2309 wants to merge 1 commit into
agronholm:masterfrom
Sohel2309:fix-1273-concurrent-aclose-forcefully

Conversation

@Sohel2309

Copy link
Copy Markdown

Summary

Fixes a race condition in SocketStream.aclose_forcefully() on the asyncio backend where the method could return before the underlying socket had actually been released.

Bug

aclose_forcefully() runs resource.aclose() inside an already-cancelled CancelScope so that the close operation can proceed during cancellation.

For asyncio SocketStream, the close sequence relied on an await sleep(0) checkpoint between transport.close() and transport.abort().

Because aclose_forcefully() pre-cancelled its scope, that checkpoint could be cancelled and transport.abort() could be skipped. This allowed aclose_forcefully() to return while the underlying socket file descriptor was still open.

There was also a related race when multiple tasks called aclose() concurrently: a later caller could observe is_closing() and return before the task performing the actual transport abort had completed.

Fix

  • Ensure transport.abort() is reached even when the close checkpoint is cancelled by wrapping the close/checkpoint sequence in try/finally.
  • Track the task performing the actual close with an asyncio.Event.
  • Make concurrent callers wait for the close operation to complete instead of relying only on the transport's is_closing() state.

This guarantees that aclose_forcefully() does not return before the underlying socket has actually been released.

Regression tests

Added regression coverage for:

  • Forcefully closing a socket while cancellation is already active.
  • Concurrent aclose() calls on the same socket.
  • Ensuring the underlying socket is actually released before the close operation returns.

The tests were verified across the supported asyncio, trio, trio-asyncio, and curio backends.

Validation

  • Regression tests pass.
  • Repeated stress runs pass consistently.
  • Broader test suite passes.
  • Formatting, linting, and type checks were run.
  • No unrelated changes are included.

Fixes #1273

… asyncio

anyio.aclose_forcefully() wraps resource.aclose() in an already-cancelled
CancelScope so that closing happens as quickly as possible. On the asyncio
backend, SocketStream.aclose() (used for TCP streams) relied on a single
'await sleep(0)' checkpoint between transport.close() and the forceful
transport.abort() call to give the transport a chance to flush. Because
aclose_forcefully() pre-cancels its scope, that checkpoint could raise a
Cancelled exception (silently swallowed by the scope), which caused
transport.abort() to be skipped entirely, leaving the underlying socket fd
open after aclose_forcefully() returned.

Separately, concurrent calls to aclose() on the same SocketStream could
race: the first caller's synchronous transport.close() call already marks
the transport as 'closing', so a second, concurrently running caller could
see is_closing() == True and return immediately without ever performing
(or waiting for) the actual abort(), again leaving the fd open at the time
it returned.

Fix:
* Wrap the close()/checkpoint sequence in try/finally so transport.abort()
  always executes, even if the checkpoint is cancelled.
* Track the close operation with an asyncio.Event. Once a task has claimed
  the role of performing the actual close, any other concurrent caller
  waits (shielded from its own forced cancellation) for that Event instead
  of taking a is_closing()-based shortcut, guaranteeing it only returns
  after the socket has actually been released.

Added a regression test, ported from the issue report, that spawns two
concurrent aclose_forcefully() calls on the same TCP stream and asserts
that the raw socket fd is released (-1) for both by the time they return.
It fails on current main and passes with this fix, across the asyncio,
asyncio+uvloop, asyncio+eager, and trio backends.

Fixes agronholm#1273.
@agronholm

Copy link
Copy Markdown
Owner

Closing in favor of #1277 which is more comprehensive. Also, you deleted the PR template which you were warned about.

@agronholm agronholm closed this Aug 15, 2026
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.

concurrent anyio.aclose_forcefully fails to block until socket is closed on asyncio

2 participants