From 5b08b135305d548270d992a09890b88461f33862 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 10 Oct 2026 22:37:03 +0300 Subject: [PATCH 1/7] gh-159049: Fix reference cycles after a failed TLS handshake in asyncio --- Lib/asyncio/base_events.py | 2 ++ Lib/asyncio/selector_events.py | 2 ++ Lib/test/test_asyncio/test_sslproto.py | 28 +++++++++++++++++++ ...-10-10-22-29-13.gh-issue-159049.eJUiKL.rst | 2 ++ 4 files changed, 34 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 90269c936555cb5..bd700a947bbcf40 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1244,6 +1244,8 @@ async def _create_connection_transport( await waiter except: transport.close() + # gh-159049 + waiter = None raise return transport, protocol diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index bf20f053ded4353..2e789d6b808d981 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1225,6 +1225,8 @@ def _call_connection_lost(self, exc): super()._call_connection_lost(exc) finally: self._write_ready = None + # gh-159049: clear callback too + 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..bbd9b74dae195f8 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -1,5 +1,6 @@ """Tests for asyncio/sslproto.py.""" +import gc import logging import socket import unittest @@ -832,6 +833,33 @@ 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) + + def server(sock): + try: + sock.start_tls(sslctx, server_side=True) + except OSError: + pass + + 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(): + self.loop.run_until_complete(conn(srv.addr)) + self.assertFalse([o for o in gc.get_objects() + if isinstance(o, (asyncio.Transport, + sslproto.SSLProtocol))]) + 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..cfb92f67c26219e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst @@ -0,0 +1,2 @@ +Fix reference cycles in :meth:asyncio.loop.create_connection after a failed +TLS handshake. From 0885bcdcd9c305ed0881b3360b532cd0224d1b50 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 10 Oct 2026 22:41:01 +0300 Subject: [PATCH 2/7] fix lint --- .../Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index cfb92f67c26219e..192e7b61cfcb725 100644 --- 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 @@ -1,2 +1,2 @@ -Fix reference cycles in :meth:asyncio.loop.create_connection after a failed -TLS handshake. +Fix reference cycles in :meth:`asyncio.loop.create_connection` after a failed +TLS handshake. Patch by Timofei Ivankov From 0454f09ef5e7b44f2dbc37a15eee522a66883a77 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 13:22:55 +0300 Subject: [PATCH 3/7] remove comments --- Lib/asyncio/selector_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 2e789d6b808d981..f41fa47b891d4b1 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1225,7 +1225,6 @@ def _call_connection_lost(self, exc): super()._call_connection_lost(exc) finally: self._write_ready = None - # gh-159049: clear callback too self._read_ready_cb = None if self._empty_waiter is not None: self._empty_waiter.set_exception( From 9ab3243dae535672190a35646cf6e310cb6d400c Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 13:29:32 +0300 Subject: [PATCH 4/7] delete comment --- Lib/asyncio/base_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index bd700a947bbcf40..760fe9e80c9e167 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1244,7 +1244,6 @@ async def _create_connection_transport( await waiter except: transport.close() - # gh-159049 waiter = None raise From 41d49968ce6b09512c048a92c17eecb6ae9cdd07 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 13:50:04 +0300 Subject: [PATCH 5/7] rewrite test with weakref instead of get_objects --- Lib/test/test_asyncio/test_sslproto.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_asyncio/test_sslproto.py b/Lib/test/test_asyncio/test_sslproto.py index bbd9b74dae195f8..956a5256a28b3e7 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -1,6 +1,5 @@ """Tests for asyncio/sslproto.py.""" -import gc import logging import socket import unittest @@ -838,6 +837,7 @@ def test_create_connection_ssl_failed_certificate_no_cycles(self): sslctx = test_utils.simple_server_sslcontext() client_sslctx = test_utils.simple_client_sslcontext( disable_verify=False) + transports = [] def server(sock): try: @@ -845,6 +845,12 @@ def server(sock): 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( @@ -854,11 +860,11 @@ async def conn(addr): pass support.gc_collect() - with self.tcp_server(server) as srv, support.disable_gc(): + 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.assertFalse([o for o in gc.get_objects() - if isinstance(o, (asyncio.Transport, - sslproto.SSLProtocol))]) + 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 From 729aca7300ade5b5f3ef877cd2edbccfd481b82f Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 18:40:45 +0300 Subject: [PATCH 6/7] break ref cycle in start_tls --- Lib/asyncio/base_events.py | 1 + Lib/test/test_asyncio/test_sslproto.py | 28 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 760fe9e80c9e167..9eb34c5f29fb551 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1382,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/test/test_asyncio/test_sslproto.py b/Lib/test/test_asyncio/test_sslproto.py index 956a5256a28b3e7..acb80c19c0bdd0a 100644 --- a/Lib/test/test_asyncio/test_sslproto.py +++ b/Lib/test/test_asyncio/test_sslproto.py @@ -866,6 +866,34 @@ async def conn(addr): 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 From d0d786de33cec544d4809b7b86bb93f2f96eae48 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 18:46:12 +0300 Subject: [PATCH 7/7] update news entry --- .../Library/2026-10-10-22-29-13.gh-issue-159049.eJUiKL.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 192e7b61cfcb725..96f1cc3c0520d57 100644 --- 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 @@ -1,2 +1,3 @@ -Fix reference cycles in :meth:`asyncio.loop.create_connection` after a failed -TLS handshake. Patch by Timofei Ivankov +Fix reference cycles in :meth:`asyncio.loop.create_connection` and +:meth:`asyncio.loop.start_tls` after a failed TLS handshake. +Patch by Timofei Ivankov.