Skip to content

Commit ffe1315

Browse files
authored
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
1 parent 23a5ba1 commit ffe1315

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
@@ -669,13 +669,21 @@ def _do_shutdown(self):
669669
self._on_shutdown_complete(None)
670670

671671
def _on_shutdown_complete(self, shutdown_exc):
672-
if self._shutdown_timeout_handle is not None:
673-
self._shutdown_timeout_handle.cancel()
674-
self._shutdown_timeout_handle = None
675-
676672
if shutdown_exc:
673+
if self._shutdown_timeout_handle is not None:
674+
self._shutdown_timeout_handle.cancel()
675+
self._shutdown_timeout_handle = None
677676
self._fatal_error(shutdown_exc)
678677
else:
678+
# transport.close() flushes its write buffer first, which a peer
679+
# that does not read can delay forever. Keep the timeout (or
680+
# start it, if the peer initiated the shutdown) until
681+
# connection_lost().
682+
if self._shutdown_timeout_handle is None:
683+
self._shutdown_timeout_handle = self._loop.call_later(
684+
self._ssl_shutdown_timeout,
685+
self._check_shutdown_timeout
686+
)
679687
self._loop.call_soon(self._transport.close)
680688

681689
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
@@ -1736,6 +1736,74 @@ async def client(addr):
17361736
with self.tcp_server(server) as srv:
17371737
loop.run_until_complete(client(srv.addr))
17381738

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

17401808
###############################################################################
17411809
# 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)