diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 90269c936555cb5..9eb34c5f29fb551 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1244,6 +1244,7 @@ async def _create_connection_transport( await waiter except: transport.close() + waiter = None raise return transport, protocol @@ -1381,6 +1382,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 bf20f053ded4353..f41fa47b891d4b1 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1225,6 +1225,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 253a470680ff052..acb80c19c0bdd0a 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -832,6 +832,68 @@ async def client(addr): self.assertIsInstance(server_err, ssl.SSLError) self.assertIn('ALERT_UNKNOWN_CA', server_err.reason or '') + 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_create_server_ssl_failed_handshake_sends_alert(self): # gh-98078: when the handshake fails, the server must send the # fatal TLS alert generated by OpenSSL to the client before 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 000000000000000..96f1cc3c0520d57 --- /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.