From 1b7c5dd3644eb7102db85ad581b5f509186c3964 Mon Sep 17 00:00:00 2001 From: fizzi01 Date: Thu, 18 Dec 2025 15:06:02 +0100 Subject: [PATCH 1/5] Transfer buffered data from old protocol to SSL layer in start_tls() --- tests/test_tcp.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++ uvloop/loop.pyx | 13 +++++++ 2 files changed, 102 insertions(+) diff --git a/tests/test_tcp.py b/tests/test_tcp.py index 80584a86..6db7c2b7 100644 --- a/tests/test_tcp.py +++ b/tests/test_tcp.py @@ -1263,6 +1263,95 @@ class _TestSSL(tb.SSLTestCase): PAYLOAD_SIZE = 1024 * 100 TIMEOUT = 60 + def test_start_tls_buffer_transfer(self): + if self.implementation == 'asyncio': + raise unittest.SkipTest() + + HELLO_MSG = b'1' * self.PAYLOAD_SIZE + BUFFERED_MSG = b'buffered data before TLS' + + server_context = self._create_server_ssl_context( + self.ONLYCERT, self.ONLYKEY) + client_context = self._create_client_ssl_context() + + async def handle_client(reader, writer): + # Send data before TLS upgrade + writer.write(BUFFERED_MSG) + await writer.drain() + await asyncio.sleep(0.2) + + # Read pre-TLS data + data = await reader.readexactly(len(HELLO_MSG)) + self.assertEqual(len(data), len(HELLO_MSG)) + + # Upgrade to TLS (server side) + try: + # We need the wait_for because the broken version hangs here + await asyncio.wait_for(writer.start_tls(server_context), + timeout=2 + ) + self.assertIsNotNone(writer.get_extra_info('sslcontext')) + except asyncio.TimeoutError: + self.assertIsNotNone(writer.get_extra_info('sslcontext')) + + # Send/receive over TLS + writer.write(b'OK') + await writer.drain() + + data = await reader.readexactly(len(HELLO_MSG)) + self.assertEqual(len(data), len(HELLO_MSG)) + + writer.close() + await self.wait_closed(writer) + + async def client(addr): + # Use open_connection for StreamReader/StreamWriter + reader, writer = await asyncio.open_connection(*addr) + + # Read buffered data before TLS + buffered = await reader.readexactly(len(BUFFERED_MSG)) + self.assertEqual(buffered, BUFFERED_MSG, + "Client didn't receive buffered data before TLS upgrade") + + # Write before TLS upgrade + writer.write(HELLO_MSG) + await writer.drain() + + # Upgrade to TLS + try: + # We need the wait_for because the broken version hangs here + await asyncio.wait_for(writer.start_tls(client_context), + timeout=2 + ) + self.assertIsNotNone(writer.get_extra_info('sslcontext')) + except asyncio.TimeoutError: + self.assertIsNotNone(writer.get_extra_info('sslcontext')) + + # Verify communication over TLS + tls_data = await reader.readexactly(2) + self.assertEqual(tls_data, b'OK', + "Client didn't receive TLS response correctly") + + # Continue over TLS + writer.write(HELLO_MSG) + await writer.drain() + + writer.close() + await self.wait_closed(writer) + + async def run_test(): + srv = await asyncio.start_server( + handle_client, '127.0.0.1', 0, family=socket.AF_INET) + + addr = srv.sockets[0].getsockname() + + await asyncio.wait_for(client(addr), timeout=10) + + srv.close() + await srv.wait_closed() + + self.loop.run_until_complete(run_test()) + def test_create_server_ssl_1(self): CNT = 0 # number of clients that were successful TOTAL_CNT = 25 # total number of clients that test will create diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index 2ed1f272..4ffc8e3a 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -1616,6 +1616,19 @@ cdef class Loop: ssl_shutdown_timeout=ssl_shutdown_timeout, call_connection_made=False) + # Transfer buffered data from the old protocol to the new one. + if not hasattr(protocol, '_stream_reader'): + return + + stream_reader = protocol._stream_reader + if stream_reader is None: + return + + buffer = stream_reader._buffer + if buffer: + ssl_protocol._incoming.write(buffer) + buffer.clear() + # Pause early so that "ssl_protocol.data_received()" doesn't # have a chance to get called before "ssl_protocol.connection_made()". transport.pause_reading() From 20189edc181c5deb5c481c0c7ad68ab0cf5016ed Mon Sep 17 00:00:00 2001 From: fizzi01 Date: Fri, 19 Dec 2025 08:29:33 +0100 Subject: [PATCH 2/5] Fix wrong return behavior when missing _stream_reader in protocol --- uvloop/loop.pyx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index 4ffc8e3a..6ed6580a 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -1617,17 +1617,15 @@ cdef class Loop: call_connection_made=False) # Transfer buffered data from the old protocol to the new one. - if not hasattr(protocol, '_stream_reader'): - return - - stream_reader = protocol._stream_reader - if stream_reader is None: - return - - buffer = stream_reader._buffer - if buffer: - ssl_protocol._incoming.write(buffer) - buffer.clear() + stream_buff = None + if hasattr(protocol, '_stream_reader'): + stream_reader = protocol._stream_reader + if stream_reader is not None: + stream_buff = getattr(stream_reader, '_buffer', None) + + if stream_buff is not None: + ssl_protocol._incoming.write(stream_buff) + stream_buff.clear() # Pause early so that "ssl_protocol.data_received()" doesn't # have a chance to get called before "ssl_protocol.connection_made()". From f8fa0cf9ed37a57fea1170f6973c200987c2d38c Mon Sep 17 00:00:00 2001 From: fizzi01 Date: Fri, 19 Dec 2025 14:20:20 +0100 Subject: [PATCH 3/5] Resolve flake8 warnings in test utilities --- tests/test_tcp.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_tcp.py b/tests/test_tcp.py index 6db7c2b7..348f531c 100644 --- a/tests/test_tcp.py +++ b/tests/test_tcp.py @@ -1287,9 +1287,9 @@ async def handle_client(reader, writer): # Upgrade to TLS (server side) try: # We need the wait_for because the broken version hangs here - await asyncio.wait_for(writer.start_tls(server_context), - timeout=2 - ) + await asyncio.wait_for( + writer.start_tls(server_context), + timeout=2) self.assertIsNotNone(writer.get_extra_info('sslcontext')) except asyncio.TimeoutError: self.assertIsNotNone(writer.get_extra_info('sslcontext')) @@ -1311,7 +1311,7 @@ async def client(addr): # Read buffered data before TLS buffered = await reader.readexactly(len(BUFFERED_MSG)) self.assertEqual(buffered, BUFFERED_MSG, - "Client didn't receive buffered data before TLS upgrade") + "Wrong pre-TLS buffered data from server") # Write before TLS upgrade writer.write(HELLO_MSG) @@ -1320,9 +1320,9 @@ async def client(addr): # Upgrade to TLS try: # We need the wait_for because the broken version hangs here - await asyncio.wait_for(writer.start_tls(client_context), - timeout=2 - ) + await asyncio.wait_for( + writer.start_tls(client_context), + timeout=2) self.assertIsNotNone(writer.get_extra_info('sslcontext')) except asyncio.TimeoutError: self.assertIsNotNone(writer.get_extra_info('sslcontext')) @@ -1330,7 +1330,7 @@ async def client(addr): # Verify communication over TLS tls_data = await reader.readexactly(2) self.assertEqual(tls_data, b'OK', - "Client didn't receive TLS response correctly") + "Wrong data from server after TLS upgrade") # Continue over TLS writer.write(HELLO_MSG) From 8d77cee0272518b1eaa63492e96fb3fc108e54c9 Mon Sep 17 00:00:00 2001 From: fizzi01 Date: Fri, 23 Jan 2026 20:35:20 +0100 Subject: [PATCH 4/5] Simplify logic --- uvloop/loop.pyx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index 6ed6580a..9dc2bda0 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -1617,15 +1617,12 @@ cdef class Loop: call_connection_made=False) # Transfer buffered data from the old protocol to the new one. - stream_buff = None - if hasattr(protocol, '_stream_reader'): - stream_reader = protocol._stream_reader - if stream_reader is not None: - stream_buff = getattr(stream_reader, '_buffer', None) - - if stream_buff is not None: - ssl_protocol._incoming.write(stream_buff) - stream_buff.clear() + stream_reader = getattr(protocol, '_stream_reader', None) + if stream_reader is not None: + stream_buff = getattr(stream_reader, '_buffer', None) + if stream_buff is not None: + ssl_protocol._incoming.write(stream_buff) + stream_buff.clear() # Pause early so that "ssl_protocol.data_received()" doesn't # have a chance to get called before "ssl_protocol.connection_made()". From a40e9c632c95beb87ef2054b20e884c4d0811137 Mon Sep 17 00:00:00 2001 From: fizzi01 Date: Fri, 23 Jan 2026 21:03:52 +0100 Subject: [PATCH 5/5] Skip test for Python versions before 3.11 --- tests/test_tcp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_tcp.py b/tests/test_tcp.py index 348f531c..4fcd7395 100644 --- a/tests/test_tcp.py +++ b/tests/test_tcp.py @@ -1264,7 +1264,8 @@ class _TestSSL(tb.SSLTestCase): TIMEOUT = 60 def test_start_tls_buffer_transfer(self): - if self.implementation == 'asyncio': + if self.implementation == 'asyncio' and sys.version_info < (3, 11): + # StreamWriter.start_tls() introduced in Python 3.11 raise unittest.SkipTest() HELLO_MSG = b'1' * self.PAYLOAD_SIZE