From aa43c3c8ed6abd8cd2d15ded18f543e554a74623 Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:25:24 +0300 Subject: [PATCH] gh-159049: Fix reference cycles after a failed TLS handshake in asyncio --- Lib/asyncio/base_events.py | 2 + Lib/asyncio/selector_events.py | 1 + Lib/test/test_asyncio/test_sslproto.py | 62 +++++++++++++++++++ ...-10-10-22-29-13.gh-issue-159049.eJUiKL.rst | 3 + 4 files changed, 68 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 7b6a67023381fe..59c61f9ba657a1 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1243,6 +1243,7 @@ async def _create_connection_transport( await waiter except: transport.close() + waiter = None raise return transport, protocol @@ -1377,6 +1378,7 @@ async def start_tls(self, transport, protocol, sslcontext, *, transport.close() conmade_cb.cancel() resume_cb.cancel() + waiter = None raise return ssl_protocol._app_transport diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 326d64025f3685..c335eaee7a7146 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1222,6 +1222,7 @@ def _call_connection_lost(self, exc): super()._call_connection_lost(exc) finally: self._write_ready = None + self._read_ready_cb = None if self._empty_waiter is not None: self._empty_waiter.set_exception( ConnectionError("Connection is closed by peer")) diff --git a/Lib/test/test_asyncio/test_sslproto.py b/Lib/test/test_asyncio/test_sslproto.py index 455397024107e6..851dc6e127651c 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -824,6 +824,68 @@ async def client(addr): with self.assertRaises(ssl.SSLCertVerificationError): self.loop.run_until_complete(client(srv.addr)) + def test_create_connection_ssl_failed_certificate_no_cycles(self): + # gh-159049 + sslctx = test_utils.simple_server_sslcontext() + client_sslctx = test_utils.simple_client_sslcontext( + disable_verify=False) + transports = [] + + def server(sock): + try: + sock.start_tls(sslctx, server_side=True) + except OSError: + pass + + connection_made = sslproto.SSLProtocol.connection_made + + def remember_transport(proto, tr): + transports.append(weakref.ref(tr)) + connection_made(proto, tr) + + async def conn(addr): + try: + await self.loop.create_connection( + asyncio.Protocol, *addr, + ssl=client_sslctx, server_hostname='') + except ssl.SSLCertVerificationError: + pass + + support.gc_collect() + with (self.tcp_server(server) as srv, support.disable_gc(), + mock.patch.object(sslproto.SSLProtocol, 'connection_made', + remember_transport)): + self.loop.run_until_complete(conn(srv.addr)) + self.assertEqual([tr() for tr in transports], [None]) + + def test_start_tls_failed_certificate_no_cycles(self): + # gh-159049 + sslctx = test_utils.simple_server_sslcontext() + client_sslctx = test_utils.simple_client_sslcontext( + disable_verify=False) + transports = [] + + def server(sock): + try: + sock.start_tls(sslctx, server_side=True) + except OSError: + pass + + async def client(addr): + tr, proto = await self.loop.create_connection( + asyncio.Protocol, *addr) + transports.append(weakref.ref(tr)) + try: + await self.loop.start_tls( + tr, proto, client_sslctx, server_hostname='') + except ssl.SSLCertVerificationError: + pass + + support.gc_collect() + with self.tcp_server(server) as srv, support.disable_gc(): + self.loop.run_until_complete(client(srv.addr)) + self.assertEqual([tr() for tr in transports], [None]) + def test_start_tls_client_corrupted_ssl(self): self.loop.set_exception_handler(lambda loop, ctx: None) diff --git a/Misc/NEWS.d/next/Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst b/Misc/NEWS.d/next/Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst new file mode 100644 index 00000000000000..96f1cc3c0520d5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst @@ -0,0 +1,3 @@ +Fix reference cycles in :meth:`asyncio.loop.create_connection` and +:meth:`asyncio.loop.start_tls` after a failed TLS handshake. +Patch by Timofei Ivankov.