https://github.com/python/cpython/commit/ffe13151c0d441884e406507cdc64d22e047fd7d
commit: ffe13151c0d441884e406507cdc64d22e047fd7d
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-24T22:53:46-07:00
summary:
gh-160520: Apply asyncio ssl_shutdown_timeout after peer EOF (GH-160521)
Keep the SSL shutdown timer running until connection_lost(), and start
it when the peer shuts down first, so that a peer which sends EOF but
does not read cannot keep a closed transport open indefinitely.
ANT-2026-81HUBBJ7
files:
A Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160520.sSdTmo.rst
M Lib/asyncio/sslproto.py
M Lib/test/test_asyncio/test_ssl.py
diff --git a/Lib/asyncio/sslproto.py b/Lib/asyncio/sslproto.py
index 84e82fc69cc5fd..0f38d4e5459b23 100644
--- a/Lib/asyncio/sslproto.py
+++ b/Lib/asyncio/sslproto.py
@@ -669,13 +669,21 @@ def _do_shutdown(self):
self._on_shutdown_complete(None)
def _on_shutdown_complete(self, shutdown_exc):
- if self._shutdown_timeout_handle is not None:
- self._shutdown_timeout_handle.cancel()
- self._shutdown_timeout_handle = None
-
if shutdown_exc:
+ if self._shutdown_timeout_handle is not None:
+ self._shutdown_timeout_handle.cancel()
+ self._shutdown_timeout_handle = None
self._fatal_error(shutdown_exc)
else:
+ # transport.close() flushes its write buffer first, which a peer
+ # that does not read can delay forever. Keep the timeout (or
+ # start it, if the peer initiated the shutdown) until
+ # connection_lost().
+ if self._shutdown_timeout_handle is None:
+ self._shutdown_timeout_handle = self._loop.call_later(
+ self._ssl_shutdown_timeout,
+ self._check_shutdown_timeout
+ )
self._loop.call_soon(self._transport.close)
def _abort(self, exc):
diff --git a/Lib/test/test_asyncio/test_ssl.py
b/Lib/test/test_asyncio/test_ssl.py
index 902dc7e04e5809..76f0ded6ddd9ef 100644
--- a/Lib/test/test_asyncio/test_ssl.py
+++ b/Lib/test/test_asyncio/test_ssl.py
@@ -1736,6 +1736,74 @@ async def client(addr):
with self.tcp_server(server) as srv:
loop.run_until_complete(client(srv.addr))
+ def test_shutdown_timeout_after_eof_from_not_reading_peer(self):
+ # ssl_shutdown_timeout also applies when the peer has sent EOF,
+ # before or after close(), but does not read what is still queued.
+ loop = self.loop
+ filled = threading.Event()
+ done = threading.Event()
+
+ def server(sock):
+ sslctx = self._create_server_ssl_context(
+ test_utils.ONLYCERT,
+ test_utils.ONLYKEY
+ )
+ sock = sslctx.wrap_socket(sock, server_side=True)
+ filled.wait(support.SHORT_TIMEOUT)
+ # sending EOF here, and never reading
+ sock.shutdown(socket.SHUT_WR)
+ done.wait(support.SHORT_TIMEOUT)
+ sock.close()
+
+ class Protocol(asyncio.Protocol):
+ def __init__(self):
+ self.fut = asyncio.Future(loop=loop)
+ self.paused = False
+
+ def pause_writing(self):
+ self.paused = True
+
+ def resume_writing(self):
+ self.paused = False
+
+ def connection_lost(self, exc):
+ if not self.fut.done():
+ self.fut.set_result(exc)
+
+ async def client(addr, close):
+ ctx = self._create_client_ssl_context()
+ tr, pr = await loop.create_connection(
+ Protocol, *addr, ssl=ctx,
+ ssl_shutdown_timeout=0.1)
+ try:
+ # Write until the transport cannot pass any more on to the
+ # peer; how much the kernel accepts varies by platform.
+ chunk = bytes(support.SOCK_MAX_SIZE)
+ for _ in range(16):
+ if pr.paused:
+ break
+ tr.write(chunk)
+ await asyncio.sleep(0)
+ else:
+ self.fail('the transport never paused writing')
+ if close:
+ tr.close()
+ filled.set()
+ async with asyncio.timeout(support.SHORT_TIMEOUT):
+ exc = await pr.fut
+ finally:
+ filled.set()
+ done.set()
+ tr.abort()
+ self.assertIsInstance(exc, TimeoutError)
+
+ for close in (True, False):
+ with self.subTest(close_before_eof=close):
+ filled.clear()
+ done.clear()
+ with self.tcp_server(server) as srv:
+ loop.run_until_complete(client(srv.addr, close))
+
###############################################################################
# Socket Testing Utilities
diff --git
a/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160520.sSdTmo.rst
b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160520.sSdTmo.rst
new file mode 100644
index 00000000000000..241087806c6209
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160520.sSdTmo.rst
@@ -0,0 +1,4 @@
+Fix :mod:`asyncio` SSL transports not applying *ssl_shutdown_timeout* when
+the peer has sent EOF but does not read the data still queued for it.
+Closing such a connection could wait indefinitely; it is now aborted with
+:exc:`TimeoutError` once the timeout expires.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]