From c1946f0383d9bf907d00039dfd0b9fac54688a6b Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:31:59 +0800 Subject: [PATCH 1/2] gh-152431: Update StreamReader transport after asyncio TLS upgrade StreamReaderProtocol._replace_transport only updated its own transport reference. After start_tls, the linked StreamReader still held the old transport, causing reads to use the wrong connection. --- Lib/asyncio/streams.py | 3 ++ Lib/test/test_asyncio/test_ssl.py | 30 +++++++++++++++++++ ...-06-29-00-00-00.gh-issue-152431.XxYyZz.rst | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-29-00-00-00.gh-issue-152431.XxYyZz.rst diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index a28c11e928f806a..ab8e8feb6cbdbd3 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -216,6 +216,9 @@ def _stream_reader(self): def _replace_transport(self, transport): self._transport = transport self._over_ssl = transport.get_extra_info('sslcontext') is not None + reader = self._stream_reader + if reader is not None: + reader._transport = transport def connection_made(self, transport): if self._reject_connection: diff --git a/Lib/test/test_asyncio/test_ssl.py b/Lib/test/test_asyncio/test_ssl.py index 902dc7e04e5809a..48d82a9d5a81493 100644 --- a/Lib/test/test_asyncio/test_ssl.py +++ b/Lib/test/test_asyncio/test_ssl.py @@ -1737,6 +1737,36 @@ async def client(addr): loop.run_until_complete(client(srv.addr)) + async def test_start_tls_updates_stream_reader_transport(self): + # gh-152431: after start_tls, the StreamReader must use the new + # transport, not the old one. + srv_ctx = test_utils.simple_server_sslcontext() + cli_ctx = test_utils.simple_client_sslcontext() + + async def handler(reader, writer): + writer.close() + + srv = await asyncio.start_server(handler, '127.0.0.1', 0) + addr = srv.sockets[0].getsockname() + + reader, writer = await asyncio.open_connection(*addr) + + old_transport = reader._transport + self.assertIsNotNone(old_transport) + + new_transport = await self.loop.start_tls( + writer.transport, writer._protocol, srv_ctx, + server_side=False, ssl_handshake_timeout=self.TIMEOUT) + + # The reader should now reference the TLS transport + self.assertIs(reader._transport, new_transport) + self.assertIsNot(reader._transport, old_transport) + + writer.close() + srv.close() + await srv.wait_closed() + + ############################################################################### # Socket Testing Utilities ############################################################################### diff --git a/Misc/NEWS.d/next/Library/2026-06-29-00-00-00.gh-issue-152431.XxYyZz.rst b/Misc/NEWS.d/next/Library/2026-06-29-00-00-00.gh-issue-152431.XxYyZz.rst new file mode 100644 index 000000000000000..3fabc772b138e71 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-00-00-00.gh-issue-152431.XxYyZz.rst @@ -0,0 +1,2 @@ +Fix :meth:`asyncio.StreamWriter.start_tls` to update the linked +:class:`~asyncio.StreamReader` transport after the TLS handshake. From 61a00cc019177086c7b81625b692f5561941b788 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:49:55 +0800 Subject: [PATCH 2/2] fix: use set_transport instead of direct _transport assignment --- Lib/asyncio/streams.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index ab8e8feb6cbdbd3..d2471af52498b62 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -218,7 +218,7 @@ def _replace_transport(self, transport): self._over_ssl = transport.get_extra_info('sslcontext') is not None reader = self._stream_reader if reader is not None: - reader._transport = transport + reader.set_transport(transport) def connection_made(self, transport): if self._reject_connection: @@ -475,7 +475,9 @@ def _wakeup_waiter(self): waiter.set_result(None) def set_transport(self, transport): - assert self._transport is None, 'Transport already set' + assert self._transport is None or self._transport is transport, ( + 'Transport already set' + ) self._transport = transport def _maybe_resume_transport(self):