Skip to content

Commit b354f56

Browse files
[3.15] gh-160520: Apply asyncio ssl_shutdown_timeout after peer EOF (GH-160521) (#160639)
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 (cherry picked from commit ffe1315) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent fa1a212 commit b354f56

3 files changed

Lines changed: 84 additions & 4 deletions

File tree

‎Lib/asyncio/sslproto.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,21 @@ def _do_shutdown(self):
659659
self._on_shutdown_complete(None)
660660

661661
def _on_shutdown_complete(self, shutdown_exc):
662-
if self._shutdown_timeout_handle is not None:
663-
self._shutdown_timeout_handle.cancel()
664-
self._shutdown_timeout_handle = None
665-
666662
if shutdown_exc:
663+
if self._shutdown_timeout_handle is not None:
664+
self._shutdown_timeout_handle.cancel()
665+
self._shutdown_timeout_handle = None
667666
self._fatal_error(shutdown_exc)
668667
else:
668+
# transport.close() flushes its write buffer first, which a peer
669+
# that does not read can delay forever. Keep the timeout (or
670+
# start it, if the peer initiated the shutdown) until
671+
# connection_lost().
672+
if self._shutdown_timeout_handle is None:
673+
self._shutdown_timeout_handle = self._loop.call_later(
674+
self._ssl_shutdown_timeout,
675+
self._check_shutdown_timeout
676+
)
669677
self._loop.call_soon(self._transport.close)
670678

671679
def _abort(self, exc):

‎Lib/test/test_asyncio/test_ssl.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,74 @@ async def client(addr):
17391739
with self.tcp_server(server) as srv:
17401740
loop.run_until_complete(client(srv.addr))
17411741

1742+
def test_shutdown_timeout_after_eof_from_not_reading_peer(self):
1743+
# ssl_shutdown_timeout also applies when the peer has sent EOF,
1744+
# before or after close(), but does not read what is still queued.
1745+
loop = self.loop
1746+
filled = threading.Event()
1747+
done = threading.Event()
1748+
1749+
def server(sock):
1750+
sslctx = self._create_server_ssl_context(
1751+
test_utils.ONLYCERT,
1752+
test_utils.ONLYKEY
1753+
)
1754+
sock = sslctx.wrap_socket(sock, server_side=True)
1755+
filled.wait(support.SHORT_TIMEOUT)
1756+
# sending EOF here, and never reading
1757+
sock.shutdown(socket.SHUT_WR)
1758+
done.wait(support.SHORT_TIMEOUT)
1759+
sock.close()
1760+
1761+
class Protocol(asyncio.Protocol):
1762+
def __init__(self):
1763+
self.fut = asyncio.Future(loop=loop)
1764+
self.paused = False
1765+
1766+
def pause_writing(self):
1767+
self.paused = True
1768+
1769+
def resume_writing(self):
1770+
self.paused = False
1771+
1772+
def connection_lost(self, exc):
1773+
if not self.fut.done():
1774+
self.fut.set_result(exc)
1775+
1776+
async def client(addr, close):
1777+
ctx = self._create_client_ssl_context()
1778+
tr, pr = await loop.create_connection(
1779+
Protocol, *addr, ssl=ctx,
1780+
ssl_shutdown_timeout=0.1)
1781+
try:
1782+
# Write until the transport cannot pass any more on to the
1783+
# peer; how much the kernel accepts varies by platform.
1784+
chunk = bytes(support.SOCK_MAX_SIZE)
1785+
for _ in range(16):
1786+
if pr.paused:
1787+
break
1788+
tr.write(chunk)
1789+
await asyncio.sleep(0)
1790+
else:
1791+
self.fail('the transport never paused writing')
1792+
if close:
1793+
tr.close()
1794+
filled.set()
1795+
async with asyncio.timeout(support.SHORT_TIMEOUT):
1796+
exc = await pr.fut
1797+
finally:
1798+
filled.set()
1799+
done.set()
1800+
tr.abort()
1801+
self.assertIsInstance(exc, TimeoutError)
1802+
1803+
for close in (True, False):
1804+
with self.subTest(close_before_eof=close):
1805+
filled.clear()
1806+
done.clear()
1807+
with self.tcp_server(server) as srv:
1808+
loop.run_until_complete(client(srv.addr, close))
1809+
17421810

17431811
###############################################################################
17441812
# Socket Testing Utilities
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :mod:`asyncio` SSL transports not applying *ssl_shutdown_timeout* when
2+
the peer has sent EOF but does not read the data still queued for it.
3+
Closing such a connection could wait indefinitely; it is now aborted with
4+
:exc:`TimeoutError` once the timeout expires.

0 commit comments

Comments
 (0)