From 33d0ea4998ac11ffd3844cc3ee1cf9a76ed1235b Mon Sep 17 00:00:00 2001 From: taras Date: Sat, 31 Jan 2026 04:23:40 +0100 Subject: [PATCH 1/9] WIP --- uvloop/handles/pipe.pxd | 2 + uvloop/handles/pipe.pyx | 2 +- uvloop/handles/stream.pyx | 1 + uvloop/includes/consts.pxi | 2 + uvloop/sslproto.pxd | 6 ++- uvloop/sslproto.pyx | 96 +++++++++++++++++++++----------------- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/uvloop/handles/pipe.pxd b/uvloop/handles/pipe.pxd index 56fc2658..94c35bbb 100644 --- a/uvloop/handles/pipe.pxd +++ b/uvloop/handles/pipe.pxd @@ -25,6 +25,8 @@ cdef class ReadUnixTransport(UVStream): cdef ReadUnixTransport new(Loop loop, object protocol, Server server, object waiter) + cpdef write(self, data) + cdef class WriteUnixTransport(UVStream): diff --git a/uvloop/handles/pipe.pyx b/uvloop/handles/pipe.pyx index 4b95ed6e..305eab40 100644 --- a/uvloop/handles/pipe.pyx +++ b/uvloop/handles/pipe.pyx @@ -156,7 +156,7 @@ cdef class ReadUnixTransport(UVStream): def get_write_buffer_size(self): raise NotImplementedError - def write(self, data): + cpdef write(self, data): raise NotImplementedError def writelines(self, list_of_data): diff --git a/uvloop/handles/stream.pyx b/uvloop/handles/stream.pyx index f8c7f694..5c443927 100644 --- a/uvloop/handles/stream.pyx +++ b/uvloop/handles/stream.pyx @@ -931,6 +931,7 @@ cdef void __uv_stream_buffered_alloc( try: (sc._protocol).get_buffer_impl( suggested_size, &uvbuf.base, &uvbuf.len) + return except BaseException as exc: # Can't call 'sc._fatal_error' or 'sc._close', libuv will SF. diff --git a/uvloop/includes/consts.pxi b/uvloop/includes/consts.pxi index 82f3c327..9b1082e0 100644 --- a/uvloop/includes/consts.pxi +++ b/uvloop/includes/consts.pxi @@ -15,6 +15,8 @@ cdef enum: LOG_THRESHOLD_FOR_CONNLOST_WRITES = 5 + + SSL_READ_DEFAULT_SIZE = 64 * 1024 SSL_READ_MAX_SIZE = 256 * 1024 diff --git a/uvloop/sslproto.pxd b/uvloop/sslproto.pxd index edc0f502..2f4f72eb 100644 --- a/uvloop/sslproto.pxd +++ b/uvloop/sslproto.pxd @@ -58,8 +58,10 @@ cdef class SSLProtocol: object _incoming_write object _outgoing object _outgoing_read - char* _ssl_buffer - size_t _ssl_buffer_len + bytearray _plain_read_buffer + bytearray _ssl_read_buffer + object _ssl_read_max_size_obj + SSLProtocolState _state size_t _conn_lost AppProtocolState _app_state diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index f76474e6..7e59710c 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -1,3 +1,7 @@ +from cpython.bytearray cimport PyByteArray_FromStringAndSize, PyByteArray_AS_STRING, PyByteArray_GET_SIZE, PyByteArray_Resize +from cpython.bytes cimport PyBytes_FromStringAndSize + + cdef _create_transport_context(server_side, server_hostname): if server_side: raise ValueError('Server side SSL needs a valid SSLContext') @@ -199,22 +203,12 @@ cdef class SSLProtocol: buffers which are ssl.MemoryBIO objects. """ - def __cinit__(self, *args, **kwargs): - self._ssl_buffer_len = SSL_READ_MAX_SIZE - self._ssl_buffer = PyMem_RawMalloc(self._ssl_buffer_len) - if not self._ssl_buffer: - raise MemoryError() - - def __dealloc__(self): - PyMem_RawFree(self._ssl_buffer) - self._ssl_buffer = NULL - self._ssl_buffer_len = 0 - def __init__(self, loop, app_protocol, sslcontext, waiter, server_side=False, server_hostname=None, call_connection_made=True, ssl_handshake_timeout=None, ssl_shutdown_timeout=None): + if ssl_handshake_timeout is None: ssl_handshake_timeout = SSL_HANDSHAKE_TIMEOUT elif ssl_handshake_timeout <= 0: @@ -261,6 +255,11 @@ cdef class SSLProtocol: self._incoming_write = self._incoming.write self._outgoing = ssl_MemoryBIO() self._outgoing_read = self._outgoing.read + + self._plain_read_buffer = PyByteArray_FromStringAndSize( + NULL, SSL_READ_DEFAULT_SIZE) + self._ssl_read_max_size_obj = SSL_READ_MAX_SIZE + self._state = UNWRAPPED self._conn_lost = 0 # Set when connection_lost called if call_connection_made: @@ -291,8 +290,12 @@ cdef class SSLProtocol: self._app_protocol_get_buffer = app_protocol.get_buffer self._app_protocol_buffer_updated = app_protocol.buffer_updated self._app_protocol_is_buffer = True + self._ssl_read_buffer = None else: self._app_protocol_is_buffer = False + if self._ssl_read_buffer is None: + self._ssl_read_buffer = PyByteArray_FromStringAndSize( + NULL, SSL_READ_MAX_SIZE) cdef _wakeup_waiter(self, exc=None): if self._waiter is None: @@ -356,21 +359,24 @@ cdef class SSLProtocol: self._handshake_timeout_handle = None cdef get_buffer_impl(self, size_t n, char** buf, size_t* buf_size): - cdef size_t want = n - if want > SSL_READ_MAX_SIZE: - want = SSL_READ_MAX_SIZE - if self._ssl_buffer_len < want: - self._ssl_buffer = PyMem_RawRealloc(self._ssl_buffer, want) - if not self._ssl_buffer: - raise MemoryError() - self._ssl_buffer_len = want - - buf[0] = self._ssl_buffer - buf_size[0] = self._ssl_buffer_len + cdef Py_ssize_t want = min(n, SSL_READ_MAX_SIZE) + + if len(self._plain_read_buffer) < want: + PyByteArray_Resize(self._plain_read_buffer, want) + if self._ssl_read_buffer is not None: + PyByteArray_Resize(self._ssl_read_buffer, want) + + buf[0] = PyByteArray_AS_STRING(self._plain_read_buffer) + buf_size[0] = PyByteArray_GET_SIZE(self._plain_read_buffer) cdef buffer_updated_impl(self, size_t nbytes): - self._incoming_write(PyMemoryView_FromMemory( - self._ssl_buffer, nbytes, PyBUF_WRITE)) + mv = PyMemoryView_FromMemory( + self._plain_read_buffer, + nbytes, + PyBUF_WRITE + ) + + self._incoming_write(mv) if self._state == DO_HANDSHAKE: self._do_handshake() @@ -597,7 +603,7 @@ cdef class SSLProtocol: bint close_notify = False try: while True: - if not self._sslobj_read(SSL_READ_MAX_SIZE): + if not self._sslobj_read(self._ssl_read_max_size_obj): close_notify = True break except ssl_SSLAgainErrors as exc: @@ -787,7 +793,7 @@ cdef class SSLProtocol: PyBUF_WRITE) last_bytes_read = self._sslobj_read( - app_buffer_size - total_bytes_read, app_buffer) + self._ssl_read_max_size_obj, app_buffer) total_bytes_read += last_bytes_read if last_bytes_read == 0: @@ -823,32 +829,36 @@ cdef class SSLProtocol: cdef _do_read__copied(self): cdef: - list data - bytes first, chunk = b'1' - bint zero = True, one = False + Py_ssize_t bytes_read = -1 + list data = None + bytes first_chunk = None, curr_chunk try: while (self._incoming.pending > 0 or self._sslobj_pending() > 0): - chunk = self._sslobj_read(SSL_READ_MAX_SIZE) - if not chunk: + bytes_read = self._sslobj_read( + self._ssl_read_max_size_obj, + self._ssl_read_buffer) + if bytes_read == 0: break - if zero: - zero = False - one = True - first = chunk - elif one: - one = False - data = [first, chunk] + + curr_chunk = PyBytes_FromStringAndSize( + PyByteArray_AS_STRING(self._ssl_read_buffer), bytes_read) + + if first_chunk is None: + first_chunk = curr_chunk + elif data is None: + data = [first_chunk, curr_chunk] else: - data.append(chunk) + data.append(curr_chunk) except ssl_SSLAgainErrors as exc: pass - if one: - self._app_protocol.data_received(first) - elif not zero: + + if data is not None: self._app_protocol.data_received(b''.join(data)) - if not chunk: + elif first_chunk is not None: + self._app_protocol.data_received(first_chunk) + elif bytes_read == 0: # close_notify self._call_eof_received() self._start_shutdown() From 5385ebe024fe753548249249fa32c1d7585f8021 Mon Sep 17 00:00:00 2001 From: taras Date: Sat, 31 Jan 2026 05:34:35 +0100 Subject: [PATCH 2/9] Cleanup --- uvloop/sslproto.pxd | 1 + uvloop/sslproto.pyx | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/uvloop/sslproto.pxd b/uvloop/sslproto.pxd index 2f4f72eb..a15989c9 100644 --- a/uvloop/sslproto.pxd +++ b/uvloop/sslproto.pxd @@ -81,6 +81,7 @@ cdef class SSLProtocol: bint _app_protocol_is_buffer object _app_protocol_get_buffer object _app_protocol_buffer_updated + object _app_protocol_data_received object _handshake_start_time object _handshake_timeout_handle diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index 7e59710c..87376c42 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -293,6 +293,7 @@ cdef class SSLProtocol: self._ssl_read_buffer = None else: self._app_protocol_is_buffer = False + self._app_protocol_data_received = app_protocol.data_received if self._ssl_read_buffer is None: self._ssl_read_buffer = PyByteArray_FromStringAndSize( NULL, SSL_READ_MAX_SIZE) @@ -361,7 +362,7 @@ cdef class SSLProtocol: cdef get_buffer_impl(self, size_t n, char** buf, size_t* buf_size): cdef Py_ssize_t want = min(n, SSL_READ_MAX_SIZE) - if len(self._plain_read_buffer) < want: + if PyByteArray_GET_SIZE(self._plain_read_buffer) < want: PyByteArray_Resize(self._plain_read_buffer, want) if self._ssl_read_buffer is not None: PyByteArray_Resize(self._ssl_read_buffer, want) @@ -371,7 +372,7 @@ cdef class SSLProtocol: cdef buffer_updated_impl(self, size_t nbytes): mv = PyMemoryView_FromMemory( - self._plain_read_buffer, + PyByteArray_AS_STRING(self._plain_read_buffer), nbytes, PyBUF_WRITE ) @@ -855,10 +856,11 @@ cdef class SSLProtocol: pass if data is not None: - self._app_protocol.data_received(b''.join(data)) + self._app_protocol_data_received(b''.join(data)) elif first_chunk is not None: - self._app_protocol.data_received(first_chunk) - elif bytes_read == 0: + self._app_protocol_data_received(first_chunk) + + if bytes_read == 0: # close_notify self._call_eof_received() self._start_shutdown() From 58ce0be749305958bfe743236c401a577cba53f2 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 00:36:55 +0100 Subject: [PATCH 3/9] Fix memory leak in SSLProtocol and test_create_connection_memory_leak test --- uvloop/sslproto.pyx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index 87376c42..7789e8dd 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -350,6 +350,9 @@ cdef class SSLProtocol: self._transport = None self._app_transport = None self._app_protocol = None + self._app_protocol_get_buffer = None + self._app_protocol_data_received = None + self._app_protocol_buffer_updated = None self._wakeup_waiter(exc) if self._shutdown_timeout_handle: @@ -860,6 +863,8 @@ cdef class SSLProtocol: elif first_chunk is not None: self._app_protocol_data_received(first_chunk) + # SSLObject.read() may return 0 instead of throwing SSLWantReadError + # This indicates that we reached EOF if bytes_read == 0: # close_notify self._call_eof_received() From d33c852ea5866aefdfb2bb43246bb85f00cd6dc6 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 00:45:40 +0100 Subject: [PATCH 4/9] Add some comments --- uvloop/sslproto.pxd | 5 +++++ uvloop/sslproto.pyx | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/uvloop/sslproto.pxd b/uvloop/sslproto.pxd index a15989c9..e7cf315c 100644 --- a/uvloop/sslproto.pxd +++ b/uvloop/sslproto.pxd @@ -58,8 +58,13 @@ cdef class SSLProtocol: object _incoming_write object _outgoing object _outgoing_read + + # Buffer for the underlying UVStream buffered reads bytearray _plain_read_buffer + # Buffer for SSLObject.read calls + # Only allocated when user pass non-buffered Protocol instance bytearray _ssl_read_buffer + # Cached long object for SSLObject.read calls object _ssl_read_max_size_obj SSLProtocolState _state diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index 7789e8dd..d3ab7e2a 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -348,6 +348,12 @@ cdef class SSLProtocol: self._loop.call_soon(self._app_protocol.connection_lost, exc) self._set_state(UNWRAPPED) self._transport = None + + # Decrease ref counters to user instances to avoid cyclic references + # between user protocol, SSLProtocol and SSLTransport. + # This helps to deallocate useless objects asap. + # If not done then some tests like test_create_connection_memory_leak + # will fail. self._app_transport = None self._app_protocol = None self._app_protocol_get_buffer = None From 649f8dee8f1dfbd38c3c13cf7724df9e13efa79c Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 00:53:57 +0100 Subject: [PATCH 5/9] Optimize and cleanup _do_read_into_void --- uvloop/sslproto.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index d3ab7e2a..8b0e03a8 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -610,17 +610,17 @@ cdef class SSLProtocol: If close_notify is received for the first time, call eof_received. """ cdef: - bint close_notify = False + bytearray buffer = PyBytes_FromStringAndSize( + NULL, SSL_READ_DEFAULT_SIZE) + Py_ssize_t bytes_read = -1 try: - while True: - if not self._sslobj_read(self._ssl_read_max_size_obj): - close_notify = True - break + while bytes_read != 0: + bytes_read = self._sslobj_read(self._ssl_read_max_size_obj, buffer) except ssl_SSLAgainErrors as exc: pass except ssl_SSLZeroReturnError: - close_notify = True - if close_notify: + bytes_read = 0 + if bytes_read == 0: self._call_eof_received(context) cdef _do_flush(self, object context=None): From 13459607bf9d42f205939ae8bd4fad66522d6665 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 00:55:48 +0100 Subject: [PATCH 6/9] Fix bytearray allocation --- uvloop/sslproto.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index 8b0e03a8..48f8f188 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -610,7 +610,7 @@ cdef class SSLProtocol: If close_notify is received for the first time, call eof_received. """ cdef: - bytearray buffer = PyBytes_FromStringAndSize( + bytearray buffer = PyByteArray_FromStringAndSize( NULL, SSL_READ_DEFAULT_SIZE) Py_ssize_t bytes_read = -1 try: From 3e1a8a8f84bf31e4e9f95225b0749f9d964d294b Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 00:56:24 +0100 Subject: [PATCH 7/9] Format code --- uvloop/sslproto.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index 48f8f188..e3394a57 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -615,7 +615,8 @@ cdef class SSLProtocol: Py_ssize_t bytes_read = -1 try: while bytes_read != 0: - bytes_read = self._sslobj_read(self._ssl_read_max_size_obj, buffer) + bytes_read = self._sslobj_read( + self._ssl_read_max_size_obj, buffer) except ssl_SSLAgainErrors as exc: pass except ssl_SSLZeroReturnError: From 64ff06ecaeb0f8c5cf23cb65925e72cfec75e45d Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 01:27:59 +0100 Subject: [PATCH 8/9] Cleanup for PR --- uvloop/handles/stream.pyx | 1 - uvloop/loop.pyx | 6 ++++-- uvloop/sslproto.pyx | 5 ----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/uvloop/handles/stream.pyx b/uvloop/handles/stream.pyx index 5c443927..f8c7f694 100644 --- a/uvloop/handles/stream.pyx +++ b/uvloop/handles/stream.pyx @@ -931,7 +931,6 @@ cdef void __uv_stream_buffered_alloc( try: (sc._protocol).get_buffer_impl( suggested_size, &uvbuf.base, &uvbuf.len) - return except BaseException as exc: # Can't call 'sc._fatal_error' or 'sc._close', libuv will SF. diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index 577d45a4..def11e6f 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -35,8 +35,10 @@ from cpython cimport Py_INCREF, Py_DECREF, Py_XDECREF, Py_XINCREF from cpython cimport ( PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, Py_buffer, PyBytes_AsString, PyBytes_CheckExact, - PyBytes_AsStringAndSize, - Py_SIZE, PyBytes_AS_STRING, PyBUF_WRITABLE + PyBytes_AsStringAndSize, PyBytes_FromStringAndSize, + Py_SIZE, PyBytes_AS_STRING, PyBUF_WRITABLE, + PyByteArray_AS_STRING, PyByteArray_GET_SIZE, PyByteArray_Resize, + PyByteArray_FromStringAndSize ) from cpython.pycapsule cimport PyCapsule_New, PyCapsule_GetPointer diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index e3394a57..b40667bf 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -1,7 +1,3 @@ -from cpython.bytearray cimport PyByteArray_FromStringAndSize, PyByteArray_AS_STRING, PyByteArray_GET_SIZE, PyByteArray_Resize -from cpython.bytes cimport PyBytes_FromStringAndSize - - cdef _create_transport_context(server_side, server_hostname): if server_side: raise ValueError('Server side SSL needs a valid SSLContext') @@ -208,7 +204,6 @@ cdef class SSLProtocol: call_connection_made=True, ssl_handshake_timeout=None, ssl_shutdown_timeout=None): - if ssl_handshake_timeout is None: ssl_handshake_timeout = SSL_HANDSHAKE_TIMEOUT elif ssl_handshake_timeout <= 0: From cde915dbf9e7c15e269f010f0d71c61aa3507d6b Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 1 Feb 2026 01:36:54 +0100 Subject: [PATCH 9/9] Cleanup cython imports --- uvloop/loop.pyx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index def11e6f..54f3f11b 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -28,18 +28,23 @@ from libc.stdint cimport uint64_t from libc.string cimport memset, strerror, memcpy from libc cimport errno -from cpython cimport PyObject -from cpython cimport PyErr_CheckSignals, PyErr_Occurred -from cpython cimport PyThread_get_thread_ident -from cpython cimport Py_INCREF, Py_DECREF, Py_XDECREF, Py_XINCREF -from cpython cimport ( - PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, - Py_buffer, PyBytes_AsString, PyBytes_CheckExact, - PyBytes_AsStringAndSize, PyBytes_FromStringAndSize, - Py_SIZE, PyBytes_AS_STRING, PyBUF_WRITABLE, +from cpython.pythread cimport PyThread_get_thread_ident +from cpython.object cimport PyObject, Py_SIZE +from cpython.exc cimport PyErr_CheckSignals, PyErr_Occurred +from cpython.buffer cimport ( + Py_buffer, PyObject_GetBuffer, PyBuffer_Release, + PyBUF_SIMPLE, PyBUF_WRITABLE +) +from cpython.ref cimport Py_INCREF, Py_DECREF, Py_XDECREF, Py_XINCREF +from cpython.bytes cimport ( + PyBytes_CheckExact, PyBytes_AS_STRING, PyBytes_AsString, + PyBytes_AsStringAndSize, PyBytes_FromStringAndSize +) +from cpython.bytearray cimport ( PyByteArray_AS_STRING, PyByteArray_GET_SIZE, PyByteArray_Resize, PyByteArray_FromStringAndSize ) + from cpython.pycapsule cimport PyCapsule_New, PyCapsule_GetPointer from . import _noop