From a018beb537a1a6ac0c1bad6283da371d052b193c Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sun, 11 Sep 2022 17:53:03 +0300 Subject: [PATCH 01/33] Add methods for checking whether kTLS is used --- Modules/_ssl.c | 38 ++++++++++++++++++++++++++++++++++++++ Modules/clinic/_ssl.c.h | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 2885774295b065..5fb832cb3a1077 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2277,6 +2277,42 @@ PySSL_select(PySocketSockObject *s, int writing, _PyTime_t timeout) return rc == 0 ? SOCKET_HAS_TIMED_OUT : SOCKET_OPERATION_OK; } +/*[clinic input] +_ssl._SSLSocket.uses_ktls_for_write + +Check if the Kernel TLS data-path is used for sending. +[clinic start generated code]*/ + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_write_impl(PySSLSocket *self) +/*[clinic end generated code: output=a6c2a790ffd0587e input=156f67420e69b2f9]*/ +{ +#ifdef BIO_get_ktls_send + int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); + return PyBool_FromLong((long)uses); +#else + return Py_False; +#endif +} + +/*[clinic input] +_ssl._SSLSocket.uses_ktls_for_read + +Check if the Kernel TLS data-path is used for receiving. +[clinic start generated code]*/ + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) +/*[clinic end generated code: output=140a75033c8316a6 input=0296846b94a57932]*/ +{ +#ifdef BIO_get_ktls_recv + int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); + return PyBool_FromLong((long)uses); +#else + return Py_False; +#endif +} + /*[clinic input] _ssl._SSLSocket.write b: Py_buffer @@ -2892,6 +2928,8 @@ static PyGetSetDef ssl_getsetlist[] = { static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_DO_HANDSHAKE_METHODDEF + _SSL__SSLSOCKET_USES_KTLS_FOR_WRITE_METHODDEF + _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF _SSL__SSLSOCKET_WRITE_METHODDEF _SSL__SSLSOCKET_READ_METHODDEF _SSL__SSLSOCKET_PENDING_METHODDEF diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 622e321fa1d8b3..02fc2d0b7e075f 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -213,6 +213,42 @@ _ssl__SSLSocket_compression(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) return _ssl__SSLSocket_compression_impl(self); } +PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_write__doc__, +"uses_ktls_for_write($self, /)\n" +"--\n" +"\n" +"Check if the Kernel TLS data-path is used for sending."); + +#define _SSL__SSLSOCKET_USES_KTLS_FOR_WRITE_METHODDEF \ + {"uses_ktls_for_write", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_write, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_write__doc__}, + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_write_impl(PySSLSocket *self); + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_write(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) +{ + return _ssl__SSLSocket_uses_ktls_for_write_impl(self); +} + +PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_read__doc__, +"uses_ktls_for_read($self, /)\n" +"--\n" +"\n" +"Check if the Kernel TLS data-path is used for receiving."); + +#define _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF \ + {"uses_ktls_for_read", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_read, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_read__doc__}, + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self); + +static PyObject * +_ssl__SSLSocket_uses_ktls_for_read(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) +{ + return _ssl__SSLSocket_uses_ktls_for_read_impl(self); +} + PyDoc_STRVAR(_ssl__SSLSocket_write__doc__, "write($self, b, /)\n" "--\n" @@ -1543,4 +1579,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=9f477b0c709acb28 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d4842f6b79d738a4 input=a9049054013a1b77]*/ From 72e7f5e196865a86a091b8222edbbe7f6a675aec Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 30 Nov 2022 20:11:51 +0200 Subject: [PATCH 02/33] Start using `SSL_sendfile` when available --- Lib/socket.py | 143 +++++++++++++++++++++------------------- Lib/ssl.py | 32 +++++++-- Modules/_ssl.c | 138 ++++++++++++++++++++++++++++++++++++++ Modules/clinic/_ssl.c.h | 74 ++++++++++++++++++++- 4 files changed, 313 insertions(+), 74 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index 1c8cef6ce65810..bd3da1e55868f2 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -54,6 +54,7 @@ import os, sys, io, selectors from enum import IntEnum, IntFlag +from functools import partial try: import errno @@ -344,76 +345,84 @@ def makefile(self, mode="r", buffering=None, *, text.mode = mode return text - if hasattr(os, 'sendfile'): + def _sendfile_zerocopy(self, zerocopy_func, giveup_err, file, offset=0, count=None): + """ + Send a file using a zero-copy function. + """ + self._check_sendfile_params(file, offset, count) + sockno = self.fileno() + try: + fileno = file.fileno() + except (AttributeError, io.UnsupportedOperation) as err: + raise giveup_err(err) # not a regular file + try: + fsize = os.fstat(fileno).st_size + except OSError as err: + raise giveup_err(err) # not a regular file + if not fsize: + return 0 # empty file + # Truncate to 1GiB to avoid OverflowError, see bpo-38319. + blocksize = min(count or fsize, 2 ** 30) + timeout = self.gettimeout() + if timeout == 0: + raise ValueError("non-blocking sockets are not supported") + # poll/select have the advantage of not requiring any + # extra file descriptor, contrarily to epoll/kqueue + # (also, they require a single syscall). + if hasattr(selectors, 'PollSelector'): + selector = selectors.PollSelector() + else: + selector = selectors.SelectSelector() + selector.register(sockno, selectors.EVENT_WRITE) - def _sendfile_use_sendfile(self, file, offset=0, count=None): - self._check_sendfile_params(file, offset, count) - sockno = self.fileno() - try: - fileno = file.fileno() - except (AttributeError, io.UnsupportedOperation) as err: - raise _GiveupOnSendfile(err) # not a regular file - try: - fsize = os.fstat(fileno).st_size - except OSError as err: - raise _GiveupOnSendfile(err) # not a regular file - if not fsize: - return 0 # empty file - # Truncate to 1GiB to avoid OverflowError, see bpo-38319. - blocksize = min(count or fsize, 2 ** 30) - timeout = self.gettimeout() - if timeout == 0: - raise ValueError("non-blocking sockets are not supported") - # poll/select have the advantage of not requiring any - # extra file descriptor, contrarily to epoll/kqueue - # (also, they require a single syscall). - if hasattr(selectors, 'PollSelector'): - selector = selectors.PollSelector() - else: - selector = selectors.SelectSelector() - selector.register(sockno, selectors.EVENT_WRITE) - - total_sent = 0 - # localize variable access to minimize overhead - selector_select = selector.select - os_sendfile = os.sendfile - try: - while True: - if timeout and not selector_select(timeout): - raise TimeoutError('timed out') - if count: - blocksize = count - total_sent - if blocksize <= 0: - break - try: - sent = os_sendfile(sockno, fileno, offset, blocksize) - except BlockingIOError: - if not timeout: - # Block until the socket is ready to send some - # data; avoids hogging CPU resources. - selector_select() - continue - except OSError as err: - if total_sent == 0: - # We can get here for different reasons, the main - # one being 'file' is not a regular mmap(2)-like - # file, in which case we'll fall back on using - # plain send(). - raise _GiveupOnSendfile(err) - raise err from None - else: - if sent == 0: - break # EOF - offset += sent - total_sent += sent - return total_sent - finally: - if total_sent > 0 and hasattr(file, 'seek'): - file.seek(offset) - else: - def _sendfile_use_sendfile(self, file, offset=0, count=None): + total_sent = 0 + # localize variable access to minimize overhead + selector_select = selector.select + try: + while True: + if timeout and not selector_select(timeout): + raise TimeoutError('timed out') + if count: + blocksize = count - total_sent + if blocksize <= 0: + break + try: + sent = zerocopy_func(fileno, offset, blocksize) + except BlockingIOError: + if not timeout: + # Block until the socket is ready to send some + # data; avoids hogging CPU resources. + selector_select() + continue + except OSError as err: + if total_sent == 0: + # We can get here for different reasons, the main + # one being 'file' is not a regular mmap(2)-like + # file, in which case we'll fall back on using + # plain send(). + raise giveup_err(err) + raise err from None + else: + if sent == 0: + break # EOF + offset += sent + total_sent += sent + return total_sent + finally: + if total_sent > 0 and hasattr(file, 'seek'): + file.seek(offset) + + def _sendfile_use_sendfile(self, file, offset=0, count=None): + if not (sendfile := getattr(os, 'sendfile', None)): raise _GiveupOnSendfile( "os.sendfile() not available on this platform") + return self._sendfile_zerocopy( + zerocopy_func=partial(sendfile, self.fileno()), + giveup_err=_GiveupOnSendfile, + file=file, + offset=offset, + count=count, + ) def _sendfile_use_send(self, file, offset=0, count=None): self._check_sendfile_params(file, offset, count) diff --git a/Lib/ssl.py b/Lib/ssl.py index 1d5873726441e4..f8fd6a7c7f4364 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -941,6 +941,10 @@ def _sslcopydoc(func): return func +class _GiveupOnSSLSendfile(Exception): + pass + + class SSLSocket(socket): """This class implements a subtype of socket.socket that wraps the underlying OS socket in an SSL context when necessary, and @@ -1181,15 +1185,31 @@ def sendall(self, data, flags=0): else: return super().sendall(data, flags) + def _sendfile_use_ssl_sendfile(self, file, offset=0, count=None): + if not (ssl_sendfile := getattr(self._sslobj, "sendfile", None)): + raise _GiveupOnSSLSendfile( + "SSL_sendfile() not available on this platform", + ) + return self._sendfile_zerocopy( + zerocopy_func=ssl_sendfile, + giveup_err=_GiveupOnSSLSendfile, + file=file, + offset=offset, + count=count, + ) + def sendfile(self, file, offset=0, count=None): - """Send a file, possibly by using os.sendfile() if this is a - clear-text socket. Return the total number of bytes sent. + """Send a file, possibly by using an efficient sendfile() call if + the system supports it. Return the total number of bytes sent. """ - if self._sslobj is not None: - return self._sendfile_use_send(file, offset, count) - else: - # os.sendfile() works with plain sockets only + if self._sslobj is None: return super().sendfile(file, offset, count) + if self._sslobj.uses_ktls_for_write(): + try: + return self._sendfile_use_ssl_sendfile(file, offset, count) + except _GiveupOnSSLSendfile: + pass + return self._sendfile_use_send(file, offset, count) def recv(self, buflen=1024, flags=0): self._checkClosed() diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 5fb832cb3a1077..2511f4c39058d5 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -67,6 +67,44 @@ +#ifdef MS_WINDOWS + typedef long long Py_off_t; +#else + typedef off_t Py_off_t; +#endif + +static int +Py_off_t_converter(PyObject *arg, void *addr) +{ +#ifdef HAVE_LARGEFILE_SUPPORT + *((Py_off_t *)addr) = PyLong_AsLongLong(arg); +#else + *((Py_off_t *)addr) = PyLong_AsLong(arg); +#endif + if (PyErr_Occurred()) + return 0; + return 1; +} + +static PyObject * +PyLong_FromPy_off_t(Py_off_t offset) +{ +#ifdef HAVE_LARGEFILE_SUPPORT + return PyLong_FromLongLong(offset); +#else + return PyLong_FromLong(offset); +#endif +} + +/*[python input] + +class Py_off_t_converter(CConverter): + type = 'Py_off_t' + converter = 'Py_off_t_converter' + +[python start generated code]*/ +/*[python end generated code: output=da39a3ee5e6b4b0d input=3fd9ca8ca6f0cbb8]*/ + struct py_ssl_error_code { const char *mnemonic; int library, reason; @@ -2313,6 +2351,105 @@ _ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) #endif } +#ifdef BIO_get_ktls_send +/*[clinic input] +_ssl._SSLSocket.sendfile + fd: int + offset: Py_off_t + size: Py_ssize_t + flags: int = 0 + / + +Write size bytes from offset in the file descriptor fd to the SSL connection. + +This method uses the zero-copy technique and returns the number of bytes +written. It should be called only when Kernel TLS is used for sending data in +the connection. + +The meaning of flags is platform dependent. +[clinic start generated code]*/ + +static PyObject * +_ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, + Py_ssize_t size, int flags) +/*[clinic end generated code: output=70ec71c2503e560b input=87f5a263b28cb164]*/ +{ + Py_ssize_t retval; + int sockstate; + _PySSLError err; + int nonblocking; + PySocketSockObject *sock = GET_SOCKET(self); + _PyTime_t timeout, deadline = 0; + int has_timeout; + + if (sock != NULL) { + if (((PyObject*)sock) == Py_None) { + _setSSLError(get_state_sock(self), + "Underlying socket connection gone", + PY_SSL_ERROR_NO_SOCKET, __FILE__, __LINE__); + return NULL; + } + Py_INCREF(sock); + + /* just in case the blocking state of the socket has been changed */ + nonblocking = (sock->sock_timeout >= 0); + BIO_set_nbio(SSL_get_rbio(self->ssl), nonblocking); + BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking); + } + + timeout = GET_SOCKET_TIMEOUT(sock); + has_timeout = (timeout > 0); + if (has_timeout) + deadline = _PyDeadline_Init(timeout); + + + do { + PySSL_BEGIN_ALLOW_THREADS + retval = SSL_sendfile(self->ssl, fd, offset, size, flags); + err = _PySSL_errno(retval < 0, self->ssl, retval); + PySSL_END_ALLOW_THREADS + self->err = err; + + if (PyErr_CheckSignals()) + goto error; + + if (has_timeout) { + timeout = _PyDeadline_Get(deadline); + } + + if (err.ssl == SSL_ERROR_WANT_READ) { + sockstate = PySSL_select(sock, 0, timeout); + } else if (err.ssl == SSL_ERROR_WANT_WRITE) { + sockstate = PySSL_select(sock, 1, timeout); + } else { + sockstate = SOCKET_OPERATION_OK; + } + + if (sockstate == SOCKET_HAS_TIMED_OUT) { + PyErr_SetString(PyExc_TimeoutError, + "The sendfile operation timed out"); + goto error; + } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { + PyErr_SetString(get_state_sock(self)->PySSLErrorObject, + "Underlying socket has been closed."); + goto error; + } else if (sockstate == SOCKET_IS_NONBLOCKING) { + break; + } + } while (err.ssl == SSL_ERROR_WANT_READ || + err.ssl == SSL_ERROR_WANT_WRITE); + + Py_XDECREF(sock); + if (PySSL_ChainExceptions(self) < 0) + return NULL; + return PyLong_FromSize_t(retval); +error: + Py_XDECREF(sock); + PySSL_ChainExceptions(self); + return NULL; +} +#endif /* SSL_sendfile */ + /*[clinic input] _ssl._SSLSocket.write b: Py_buffer @@ -2930,6 +3067,7 @@ static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_DO_HANDSHAKE_METHODDEF _SSL__SSLSOCKET_USES_KTLS_FOR_WRITE_METHODDEF _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF + _SSL__SSLSOCKET_SENDFILE_METHODDEF _SSL__SSLSOCKET_WRITE_METHODDEF _SSL__SSLSOCKET_READ_METHODDEF _SSL__SSLSOCKET_PENDING_METHODDEF diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 02fc2d0b7e075f..1e247fdf5eebe6 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -249,6 +249,74 @@ _ssl__SSLSocket_uses_ktls_for_read(PySSLSocket *self, PyObject *Py_UNUSED(ignore return _ssl__SSLSocket_uses_ktls_for_read_impl(self); } +#if defined(BIO_get_ktls_send) + +PyDoc_STRVAR(_ssl__SSLSocket_sendfile__doc__, +"sendfile($self, fd, offset, size, flags=0, /)\n" +"--\n" +"\n" +"Write size bytes from offset in the file descriptor fd to the SSL connection.\n" +"\n" +"This method uses the zero-copy technique and returns the number of bytes\n" +"written. It should be called only when Kernel TLS is used for sending data in\n" +"the connection.\n" +"\n" +"The meaning of flags is platform dependent."); + +#define _SSL__SSLSOCKET_SENDFILE_METHODDEF \ + {"sendfile", _PyCFunction_CAST(_ssl__SSLSocket_sendfile), METH_FASTCALL, _ssl__SSLSocket_sendfile__doc__}, + +static PyObject * +_ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, + Py_ssize_t size, int flags); + +static PyObject * +_ssl__SSLSocket_sendfile(PySSLSocket *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int fd; + Py_off_t offset; + Py_ssize_t size; + int flags = 0; + + if (!_PyArg_CheckPositional("sendfile", nargs, 3, 4)) { + goto exit; + } + fd = _PyLong_AsInt(args[0]); + if (fd == -1 && PyErr_Occurred()) { + goto exit; + } + if (!Py_off_t_converter(args[1], &offset)) { + goto exit; + } + { + Py_ssize_t ival = -1; + PyObject *iobj = _PyNumber_Index(args[2]); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + goto exit; + } + size = ival; + } + if (nargs < 4) { + goto skip_optional; + } + flags = _PyLong_AsInt(args[3]); + if (flags == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = _ssl__SSLSocket_sendfile_impl(self, fd, offset, size, flags); + +exit: + return return_value; +} + +#endif /* defined(BIO_get_ktls_send) */ + PyDoc_STRVAR(_ssl__SSLSocket_write__doc__, "write($self, b, /)\n" "--\n" @@ -1572,6 +1640,10 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #endif /* defined(_MSC_VER) */ +#ifndef _SSL__SSLSOCKET_SENDFILE_METHODDEF + #define _SSL__SSLSOCKET_SENDFILE_METHODDEF +#endif /* !defined(_SSL__SSLSOCKET_SENDFILE_METHODDEF) */ + #ifndef _SSL_ENUM_CERTIFICATES_METHODDEF #define _SSL_ENUM_CERTIFICATES_METHODDEF #endif /* !defined(_SSL_ENUM_CERTIFICATES_METHODDEF) */ @@ -1579,4 +1651,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=d4842f6b79d738a4 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a8f9790ad7a68d46 input=a9049054013a1b77]*/ From cf24d786e6d28f71b8270ad86cc579e0299ce958 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 5 Dec 2022 21:35:42 +0200 Subject: [PATCH 03/33] Try fixing warnings --- Modules/_ssl.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 2511f4c39058d5..b7c41cadcf795e 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -67,6 +67,7 @@ +#ifdef BIO_get_ktls_send #ifdef MS_WINDOWS typedef long long Py_off_t; #else @@ -86,16 +87,6 @@ Py_off_t_converter(PyObject *arg, void *addr) return 1; } -static PyObject * -PyLong_FromPy_off_t(Py_off_t offset) -{ -#ifdef HAVE_LARGEFILE_SUPPORT - return PyLong_FromLongLong(offset); -#else - return PyLong_FromLong(offset); -#endif -} - /*[python input] class Py_off_t_converter(CConverter): @@ -104,6 +95,7 @@ class Py_off_t_converter(CConverter): [python start generated code]*/ /*[python end generated code: output=da39a3ee5e6b4b0d input=3fd9ca8ca6f0cbb8]*/ +#endif /* BIO_get_ktls_send */ struct py_ssl_error_code { const char *mnemonic; @@ -2448,7 +2440,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, PySSL_ChainExceptions(self); return NULL; } -#endif /* SSL_sendfile */ +#endif /* BIO_get_ktls_send */ /*[clinic input] _ssl._SSLSocket.write From e9ef7479ef79e03b2bbd3f097d3fb2ecce0622f4 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 5 Dec 2022 21:42:40 +0200 Subject: [PATCH 04/33] Use unsigned `size_t` instead of `Py_ssize_t` --- Modules/_ssl.c | 6 +++--- Modules/clinic/_ssl.c.h | 19 +++++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b7c41cadcf795e..7d5ecd3cac0be5 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2348,7 +2348,7 @@ _ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) _ssl._SSLSocket.sendfile fd: int offset: Py_off_t - size: Py_ssize_t + size: size_t flags: int = 0 / @@ -2363,8 +2363,8 @@ The meaning of flags is platform dependent. static PyObject * _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, - Py_ssize_t size, int flags) -/*[clinic end generated code: output=70ec71c2503e560b input=87f5a263b28cb164]*/ + size_t size, int flags) +/*[clinic end generated code: output=0c6815b0719ca8d5 input=f09170aab5a44ec0]*/ { Py_ssize_t retval; int sockstate; diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 1e247fdf5eebe6..95bf77bb9030c4 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -268,7 +268,7 @@ PyDoc_STRVAR(_ssl__SSLSocket_sendfile__doc__, static PyObject * _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, - Py_ssize_t size, int flags); + size_t size, int flags); static PyObject * _ssl__SSLSocket_sendfile(PySSLSocket *self, PyObject *const *args, Py_ssize_t nargs) @@ -276,7 +276,7 @@ _ssl__SSLSocket_sendfile(PySSLSocket *self, PyObject *const *args, Py_ssize_t na PyObject *return_value = NULL; int fd; Py_off_t offset; - Py_ssize_t size; + size_t size; int flags = 0; if (!_PyArg_CheckPositional("sendfile", nargs, 3, 4)) { @@ -289,17 +289,8 @@ _ssl__SSLSocket_sendfile(PySSLSocket *self, PyObject *const *args, Py_ssize_t na if (!Py_off_t_converter(args[1], &offset)) { goto exit; } - { - Py_ssize_t ival = -1; - PyObject *iobj = _PyNumber_Index(args[2]); - if (iobj != NULL) { - ival = PyLong_AsSsize_t(iobj); - Py_DECREF(iobj); - } - if (ival == -1 && PyErr_Occurred()) { - goto exit; - } - size = ival; + if (!_PyLong_Size_t_Converter(args[2], &size)) { + goto exit; } if (nargs < 4) { goto skip_optional; @@ -1651,4 +1642,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=a8f9790ad7a68d46 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c4cc7cf648add6eb input=a9049054013a1b77]*/ From 14832de617a823d969d1caeb59b5aef58b2e09fc Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 13 Mar 2023 22:13:24 +0200 Subject: [PATCH 05/33] Make `sendfile_impl` more similar to `write_impl` --- Modules/_ssl.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b6e0b48106e3e4..41605520a42c28 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2386,7 +2386,9 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, return NULL; } Py_INCREF(sock); + } + if (sock != NULL) { /* just in case the blocking state of the socket has been changed */ nonblocking = (sock->sock_timeout >= 0); BIO_set_nbio(SSL_get_rbio(self->ssl), nonblocking); @@ -2395,9 +2397,24 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); - if (has_timeout) + if (has_timeout) { deadline = _PyDeadline_Init(timeout); + } + sockstate = PySSL_select(sock, 1, timeout); + if (sockstate == SOCKET_HAS_TIMED_OUT) { + PyErr_SetString(PyExc_TimeoutError, + "The write operation timed out"); + goto error; + } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { + PyErr_SetString(get_state_sock(self)->PySSLErrorObject, + "Underlying socket has been closed."); + goto error; + } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) { + PyErr_SetString(get_state_sock(self)->PySSLErrorObject, + "Underlying socket too large for select()."); + goto error; + } do { PySSL_BEGIN_ALLOW_THREADS From 6b41d31e94191bad7ae218ed40a9ac219d4ddaad Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 13 Mar 2023 22:18:53 +0200 Subject: [PATCH 06/33] Modify `test_ssl` to test `SSL_sendfile` calls --- Lib/test/test_ssl.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index d4eb2d2e81fe0f..c7e2a04009c4c0 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4068,19 +4068,25 @@ def test_read_write_after_close_raises_valuerror(self): self.assertRaises(ValueError, s.write, b'hello') def test_sendfile(self): - TEST_DATA = b"x" * 512 + TEST_DATA = b"GET / HTTP/1.1\r\n\r\n" with open(os_helper.TESTFN, 'wb') as f: f.write(TEST_DATA) self.addCleanup(os_helper.unlink, os_helper.TESTFN) - client_context, server_context, hostname = testing_context() - server = ThreadedEchoServer(context=server_context, chatty=False) - with server: - with client_context.wrap_socket(socket.socket(), - server_hostname=hostname) as s: - s.connect((HOST, server.port)) + client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + client_context.options |= ssl.OP_ENABLE_KTLS + client_context.check_hostname = False + client_context.verify_mode = ssl.CERT_NONE + with socket.create_connection(("www.python.org", 443)) as sock: + with client_context.wrap_socket(sock) as ssock: + if support.verbose: + ktls_used = ssock._sslobj.uses_ktls_for_write() + print( + "kTLS is", + "available" if ktls_used else "unavailable", + ) with open(os_helper.TESTFN, 'rb') as file: - s.sendfile(file) - self.assertEqual(s.recv(1024), TEST_DATA) + ssock.sendfile(file) + self.assertEqual(ssock.recv(9), b"HTTP/1.1 ") def test_session(self): client_context, server_context, hostname = testing_context() From 8ac6ff97b4993dec01cdf248653720ae365a2706 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 13 Mar 2023 22:54:01 +0200 Subject: [PATCH 07/33] Modify documentation and add a news entry --- Doc/library/ssl.rst | 10 ++++++++-- .../2023-03-13-22-51-40.gh-issue-99813.40TV02.rst | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 30f2a0765cc955..3eb8a727dada1b 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1037,8 +1037,9 @@ SSL Sockets (but passing a non-zero ``flags`` argument is not allowed) - :meth:`~socket.socket.send()`, :meth:`~socket.socket.sendall()` (with the same limitation) - - :meth:`~socket.socket.sendfile()` (but :mod:`os.sendfile` will be used - for plain-text sockets only, else :meth:`~socket.socket.send()` will be used) + - :meth:`~socket.socket.sendfile()` (but it may be high-performant only when + the kernel TLS is enabled (see :data:`~ssl.OP_ENABLE_KTLS`) or when a + socket is plain-text, else :meth:`~socket.socket.send()` will be used) - :meth:`~socket.socket.shutdown()` However, since the SSL (and TLS) protocol has its own framing atop @@ -1072,6 +1073,11 @@ SSL Sockets functions support reading and writing of data larger than 2 GB. Writing zero-length data no longer fails with a protocol violation error. + .. versionchanged:: 3.12 + Python now uses ``SSL_sendifle`` internally when it is possible. The + function sends a file more efficiently because it performs TLS encryption + in the kernel to avoid additional context switches. + SSL sockets also have the following additional methods and attributes: .. method:: SSLSocket.read(len=1024, buffer=None) diff --git a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst new file mode 100644 index 00000000000000..62bf1cab4665d7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst @@ -0,0 +1,4 @@ +Python now uses ``SSL_sendifle`` internally when it is possible (see +:data:`~ssl.OP_ENABLE_KTLS`.) The function sends a file more efficiently +because it performs TLS encryption in the kernel to avoid additional context +switches. Patch by Illia Volochii. From 5cf94837e9a3f01c9f62d48c14cee400b5715047 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 13 Mar 2023 22:55:14 +0200 Subject: [PATCH 08/33] Fix a test --- Lib/test/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index c7e2a04009c4c0..b73cb65068466b 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4073,7 +4073,7 @@ def test_sendfile(self): f.write(TEST_DATA) self.addCleanup(os_helper.unlink, os_helper.TESTFN) client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - client_context.options |= ssl.OP_ENABLE_KTLS + client_context.options |= getattr(ssl, "OP_ENABLE_KTLS", 0) client_context.check_hostname = False client_context.verify_mode = ssl.CERT_NONE with socket.create_connection(("www.python.org", 443)) as sock: From fc8e82f22b92470b045940c1ac28afd3e4c68599 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 13 Mar 2023 23:03:17 +0200 Subject: [PATCH 09/33] Rename `uses_ktls_for_write` to `uses_ktls_for_send` --- Lib/ssl.py | 2 +- Lib/test/test_ssl.py | 2 +- Modules/_ssl.c | 8 ++++---- Modules/clinic/_ssl.c.h | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index f8fd6a7c7f4364..598cc379f2537b 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1204,7 +1204,7 @@ def sendfile(self, file, offset=0, count=None): """ if self._sslobj is None: return super().sendfile(file, offset, count) - if self._sslobj.uses_ktls_for_write(): + if self._sslobj.uses_ktls_for_send(): try: return self._sendfile_use_ssl_sendfile(file, offset, count) except _GiveupOnSSLSendfile: diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index b73cb65068466b..6e860d00149d2d 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4079,7 +4079,7 @@ def test_sendfile(self): with socket.create_connection(("www.python.org", 443)) as sock: with client_context.wrap_socket(sock) as ssock: if support.verbose: - ktls_used = ssock._sslobj.uses_ktls_for_write() + ktls_used = ssock._sslobj.uses_ktls_for_send() print( "kTLS is", "available" if ktls_used else "unavailable", diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 41605520a42c28..e9cab11a5f9a2a 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2312,14 +2312,14 @@ PySSL_select(PySocketSockObject *s, int writing, _PyTime_t timeout) } /*[clinic input] -_ssl._SSLSocket.uses_ktls_for_write +_ssl._SSLSocket.uses_ktls_for_send Check if the Kernel TLS data-path is used for sending. [clinic start generated code]*/ static PyObject * -_ssl__SSLSocket_uses_ktls_for_write_impl(PySSLSocket *self) -/*[clinic end generated code: output=a6c2a790ffd0587e input=156f67420e69b2f9]*/ +_ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) +/*[clinic end generated code: output=f9d95fbefceb5068 input=604d98b67c65e8a7]*/ { #ifdef BIO_get_ktls_send int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); @@ -3078,7 +3078,7 @@ static PyGetSetDef ssl_getsetlist[] = { static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_DO_HANDSHAKE_METHODDEF - _SSL__SSLSOCKET_USES_KTLS_FOR_WRITE_METHODDEF + _SSL__SSLSOCKET_USES_KTLS_FOR_SEND_METHODDEF _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF _SSL__SSLSOCKET_SENDFILE_METHODDEF _SSL__SSLSOCKET_WRITE_METHODDEF diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 4d3d2b3f3307eb..29c7f624d33780 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -213,22 +213,22 @@ _ssl__SSLSocket_compression(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) return _ssl__SSLSocket_compression_impl(self); } -PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_write__doc__, -"uses_ktls_for_write($self, /)\n" +PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_send__doc__, +"uses_ktls_for_send($self, /)\n" "--\n" "\n" "Check if the Kernel TLS data-path is used for sending."); -#define _SSL__SSLSOCKET_USES_KTLS_FOR_WRITE_METHODDEF \ - {"uses_ktls_for_write", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_write, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_write__doc__}, +#define _SSL__SSLSOCKET_USES_KTLS_FOR_SEND_METHODDEF \ + {"uses_ktls_for_send", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_send, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_send__doc__}, static PyObject * -_ssl__SSLSocket_uses_ktls_for_write_impl(PySSLSocket *self); +_ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self); static PyObject * -_ssl__SSLSocket_uses_ktls_for_write(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) +_ssl__SSLSocket_uses_ktls_for_send(PySSLSocket *self, PyObject *Py_UNUSED(ignored)) { - return _ssl__SSLSocket_uses_ktls_for_write_impl(self); + return _ssl__SSLSocket_uses_ktls_for_send_impl(self); } PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_read__doc__, @@ -1641,4 +1641,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=5ae68c81b5201df3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=ee707ef11d50ae65 input=a9049054013a1b77]*/ From 1ba04ae9e5d4ce6949c1dca2b175b4affaed9981 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Tue, 25 Jul 2023 21:54:00 +0300 Subject: [PATCH 10/33] Update `versionchanged` --- Doc/library/ssl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 3edd296421544e..0df03ba3430ab1 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1073,7 +1073,7 @@ SSL Sockets functions support reading and writing of data larger than 2 GB. Writing zero-length data no longer fails with a protocol violation error. - .. versionchanged:: 3.12 + .. versionchanged:: 3.13 Python now uses ``SSL_sendifle`` internally when it is possible. The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. From 365e0c050dadb38b392273b6b236705747a5d850 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Tue, 25 Jul 2023 21:59:27 +0300 Subject: [PATCH 11/33] Update a test to get successful HTTP responses --- Lib/test/test_ssl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index edeb74950d035a..63c6f53e86ce74 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4106,7 +4106,8 @@ def test_read_write_after_close_raises_valuerror(self): self.assertRaises(ValueError, s.write, b'hello') def test_sendfile(self): - TEST_DATA = b"GET / HTTP/1.1\r\n\r\n" + host = "www.python.org" + TEST_DATA = b"GET / HTTP/1.1\r\nHost: %b\r\n\r\n" % host.encode() with open(os_helper.TESTFN, 'wb') as f: f.write(TEST_DATA) self.addCleanup(os_helper.unlink, os_helper.TESTFN) @@ -4114,7 +4115,7 @@ def test_sendfile(self): client_context.options |= getattr(ssl, "OP_ENABLE_KTLS", 0) client_context.check_hostname = False client_context.verify_mode = ssl.CERT_NONE - with socket.create_connection(("www.python.org", 443)) as sock: + with socket.create_connection((host, 443)) as sock: with client_context.wrap_socket(sock) as ssock: if support.verbose: ktls_used = ssock._sslobj.uses_ktls_for_send() From 8fbc955c6386893776aef5536183a238c7bfbf9b Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Tue, 25 Jul 2023 23:46:23 +0300 Subject: [PATCH 12/33] Add setting errors --- Modules/_ssl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ab25daa26a0c89..4115f143c51215 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2468,7 +2468,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, do { PySSL_BEGIN_ALLOW_THREADS retval = SSL_sendfile(self->ssl, fd, offset, size, flags); - err = _PySSL_errno(retval < 0, self->ssl, retval); + err = _PySSL_errno(retval <= 0, self->ssl, retval); PySSL_END_ALLOW_THREADS self->err = err; @@ -2502,6 +2502,8 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, err.ssl == SSL_ERROR_WANT_WRITE); Py_XDECREF(sock); + if (retval <= 0) + return PySSL_SetError(self, retval, __FILE__, __LINE__); if (PySSL_ChainExceptions(self) < 0) return NULL; return PyLong_FromSize_t(retval); From 21ea0ca3f10929dd3fe81455998253d3687bf1b0 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 26 Jul 2023 00:39:19 +0300 Subject: [PATCH 13/33] Fix conditions --- Modules/_ssl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 4115f143c51215..12ebf2aaa250d7 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2468,7 +2468,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, do { PySSL_BEGIN_ALLOW_THREADS retval = SSL_sendfile(self->ssl, fd, offset, size, flags); - err = _PySSL_errno(retval <= 0, self->ssl, retval); + err = _PySSL_errno(retval < 0, self->ssl, retval); PySSL_END_ALLOW_THREADS self->err = err; @@ -2502,7 +2502,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, err.ssl == SSL_ERROR_WANT_WRITE); Py_XDECREF(sock); - if (retval <= 0) + if (retval < 0) return PySSL_SetError(self, retval, __FILE__, __LINE__); if (PySSL_ChainExceptions(self) < 0) return NULL; From 17d9685045b41d5457e577ed6ed54e9e7cac23b7 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 26 Jul 2023 18:33:47 +0300 Subject: [PATCH 14/33] Add handling of `SSL_R_UNINITIALIZED` This is similar to https://github.com/nginx/nginx/blob/1c6183725247024f1bca73ac9a833098af7558af/src/event/ngx_event_openssl.c#L3078-L3089 --- Modules/_ssl.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 12ebf2aaa250d7..08bdddf59940b6 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2501,6 +2501,16 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, } while (err.ssl == SSL_ERROR_WANT_READ || err.ssl == SSL_ERROR_WANT_WRITE); + if (err.ssl == SSL_ERROR_SSL + && ERR_GET_REASON(ERR_peek_error()) == SSL_R_UNINITIALIZED) { + /* OpenSSL fails to return SSL_ERROR_SYSCALL if an error + * happens in sendfile(), and returns SSL_ERROR_SSL with + * SSL_R_UNINITIALIZED reason instead. */ + _setSSLError(get_state_sock(self), + "Some I/O error occurred in sendfile()", + PY_SSL_ERROR_SYSCALL, __FILE__, __LINE__); + goto error; + } Py_XDECREF(sock); if (retval < 0) return PySSL_SetError(self, retval, __FILE__, __LINE__); From 4e8b6d828b554a93b54c36fcb00f8c5cf089cea8 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 26 Jul 2023 20:12:25 +0300 Subject: [PATCH 15/33] Modify `test_sendfile` to avoid the internet --- Lib/test/test_ssl.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 63c6f53e86ce74..ef7dd2cbad4a32 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4106,26 +4106,27 @@ def test_read_write_after_close_raises_valuerror(self): self.assertRaises(ValueError, s.write, b'hello') def test_sendfile(self): - host = "www.python.org" - TEST_DATA = b"GET / HTTP/1.1\r\nHost: %b\r\n\r\n" % host.encode() + """Try to send a file using kTLS if possible.""" + TEST_DATA = b"x" * 512 with open(os_helper.TESTFN, 'wb') as f: f.write(TEST_DATA) self.addCleanup(os_helper.unlink, os_helper.TESTFN) - client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - client_context.options |= getattr(ssl, "OP_ENABLE_KTLS", 0) - client_context.check_hostname = False - client_context.verify_mode = ssl.CERT_NONE - with socket.create_connection((host, 443)) as sock: - with client_context.wrap_socket(sock) as ssock: - if support.verbose: - ktls_used = ssock._sslobj.uses_ktls_for_send() - print( - "kTLS is", - "available" if ktls_used else "unavailable", - ) - with open(os_helper.TESTFN, 'rb') as file: - ssock.sendfile(file) - self.assertEqual(ssock.recv(9), b"HTTP/1.1 ") + client_context, server_context, hostname = testing_context() + client_context.options |= getattr(ssl, 'OP_ENABLE_KTLS', 0) + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with socket.create_connection((HOST, server.port)) as sock: + with client_context.wrap_socket(sock, + server_hostname=hostname) as s: + if support.verbose: + ktls_used = s._sslobj.uses_ktls_for_send() + print( + 'kTLS is', + 'available' if ktls_used else 'unavailable', + ) + with open(os_helper.TESTFN, 'rb') as file: + s.sendfile(file) + self.assertEqual(s.recv(1024), TEST_DATA) def test_session(self): client_context, server_context, hostname = testing_context() From 379d2421c50d11293e332eaf5ea63936fbd64cce Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 26 Jul 2023 20:58:31 +0300 Subject: [PATCH 16/33] Refactor `test_sendfile` a bit --- Lib/test/test_ssl.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index ef7dd2cbad4a32..3d97bcc5ae8f32 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4114,19 +4114,22 @@ def test_sendfile(self): client_context, server_context, hostname = testing_context() client_context.options |= getattr(ssl, 'OP_ENABLE_KTLS', 0) server = ThreadedEchoServer(context=server_context, chatty=False) - with server: - with socket.create_connection((HOST, server.port)) as sock: - with client_context.wrap_socket(sock, - server_hostname=hostname) as s: - if support.verbose: - ktls_used = s._sslobj.uses_ktls_for_send() - print( - 'kTLS is', - 'available' if ktls_used else 'unavailable', - ) - with open(os_helper.TESTFN, 'rb') as file: - s.sendfile(file) - self.assertEqual(s.recv(1024), TEST_DATA) + # kTLS seems to work only with a connection created before + # wrapping `sock` by the SSL context in contrast to calling + # `sock.connect()` after the wrapping. + with server, socket.create_connection((HOST, server.port)) as sock: + with client_context.wrap_socket( + sock, server_hostname=hostname + ) as ssock: + if support.verbose: + ktls_used = ssock._sslobj.uses_ktls_for_send() + print( + 'kTLS is', + 'available' if ktls_used else 'unavailable', + ) + with open(os_helper.TESTFN, 'rb') as file: + ssock.sendfile(file) + self.assertEqual(ssock.recv(1024), TEST_DATA) def test_session(self): client_context, server_context, hostname = testing_context() From 7323ec0dc48590c4fee9821f1d1ff84a05bb4f4e Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Thu, 19 Oct 2023 23:35:29 +0300 Subject: [PATCH 17/33] Try to fix new warnings --- Modules/_ssl.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 58db78bcf2c002..6fb0cd2a405cda 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2472,8 +2472,8 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, do { PySSL_BEGIN_ALLOW_THREADS - retval = SSL_sendfile(self->ssl, fd, offset, size, flags); - err = _PySSL_errno(retval < 0, self->ssl, retval); + retval = SSL_sendfile(self->ssl, fd, (off_t)offset, size, flags); + err = _PySSL_errno(retval < 0, self->ssl, (int)retval); PySSL_END_ALLOW_THREADS self->err = err; @@ -2518,7 +2518,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, } Py_XDECREF(sock); if (retval < 0) - return PySSL_SetError(self, retval, __FILE__, __LINE__); + return PySSL_SetError(self, (int)retval, __FILE__, __LINE__); if (PySSL_ChainExceptions(self) < 0) return NULL; return PyLong_FromSize_t(retval); From e435b9a2cc93b6a26ff13854700c852437cb8edb Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 27 Mar 2024 15:52:18 +0200 Subject: [PATCH 18/33] Apply a change from ea9a296fce2f786b4cf43c7924e5de01061f27ca --- Modules/_ssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 0c0935b8d0df23..933f266a330a80 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2509,7 +2509,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, } Py_XDECREF(sock); if (retval < 0) - return PySSL_SetError(self, (int)retval, __FILE__, __LINE__); + return PySSL_SetError(self, __FILE__, __LINE__); if (PySSL_ChainExceptions(self) < 0) return NULL; return PyLong_FromSize_t(retval); From 291a5b7d4081f7baab48bf8a944f7d31f01b27b1 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 27 Dec 2024 21:43:54 +0200 Subject: [PATCH 19/33] Set `versionchanged` to next in docs --- Doc/library/ssl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 39643a227f1a6c..54da09f197b8fd 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1107,7 +1107,7 @@ SSL Sockets functions support reading and writing of data larger than 2 GB. Writing zero-length data no longer fails with a protocol violation error. - .. versionchanged:: 3.13 + .. versionchanged:: next Python now uses ``SSL_sendifle`` internally when it is possible. The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. From 94e522ee82d0328bbd3e8db281310d32245514f9 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sat, 12 Apr 2025 16:27:20 +0300 Subject: [PATCH 20/33] Apply some suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- ...3-03-13-22-51-40.gh-issue-99813.40TV02.rst | 2 +- Modules/_ssl.c | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst index 62bf1cab4665d7..5d5287e027ee20 100644 --- a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst +++ b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst @@ -1,4 +1,4 @@ -Python now uses ``SSL_sendifle`` internally when it is possible (see +Python now uses ``SSL_sendfile`` internally when it is possible (see :data:`~ssl.OP_ENABLE_KTLS`.) The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. Patch by Illia Volochii. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index f14c6fc0fd1e90..4829b78fbcd0be 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -74,7 +74,6 @@ #endif - #ifdef BIO_get_ktls_send #ifdef MS_WINDOWS typedef long long Py_off_t; @@ -90,9 +89,7 @@ Py_off_t_converter(PyObject *arg, void *addr) #else *((Py_off_t *)addr) = PyLong_AsLong(arg); #endif - if (PyErr_Occurred()) - return 0; - return 1; + return PyErr_Occurred() ? 0 : 1; } /*[python input] @@ -2482,9 +2479,9 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) { #ifdef BIO_get_ktls_send int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); - return PyBool_FromLong((long)uses); + return PyBool_FromLong(uses); #else - return Py_False; + Py_RETURN_FALSE; #endif } @@ -2500,9 +2497,9 @@ _ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) { #ifdef BIO_get_ktls_recv int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); - return PyBool_FromLong((long)uses); + return PyBool_FromLong(uses); #else - return Py_False; + Py_RETURN_FALSE; #endif } @@ -2539,7 +2536,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, int has_timeout; if (sock != NULL) { - if (((PyObject*)sock) == Py_None) { + if ((PyObject *)sock == Py_None) { _setSSLError(get_state_sock(self), "Underlying socket connection gone", PY_SSL_ERROR_NO_SOCKET, __FILE__, __LINE__); @@ -2583,8 +2580,9 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, PySSL_END_ALLOW_THREADS self->err = err; - if (PyErr_CheckSignals()) + if (PyErr_CheckSignals()) { goto error; + } if (has_timeout) { timeout = _PyDeadline_Get(deadline); @@ -2613,7 +2611,8 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, err.ssl == SSL_ERROR_WANT_WRITE); if (err.ssl == SSL_ERROR_SSL - && ERR_GET_REASON(ERR_peek_error()) == SSL_R_UNINITIALIZED) { + && ERR_GET_REASON(ERR_peek_error()) == SSL_R_UNINITIALIZED) + { /* OpenSSL fails to return SSL_ERROR_SYSCALL if an error * happens in sendfile(), and returns SSL_ERROR_SSL with * SSL_R_UNINITIALIZED reason instead. */ @@ -2623,14 +2622,16 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, goto error; } Py_XDECREF(sock); - if (retval < 0) + if (retval < 0) { return PySSL_SetError(self, __FILE__, __LINE__); - if (PySSL_ChainExceptions(self) < 0) + } + if (PySSL_ChainExceptions(self) < 0) { return NULL; + } return PyLong_FromSize_t(retval); error: Py_XDECREF(sock); - PySSL_ChainExceptions(self); + (void)PySSL_ChainExceptions(self); return NULL; } #endif /* BIO_get_ktls_send */ From 25bde6fb1aba7ec2cfe91820700c16cda5110721 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sat, 12 Apr 2025 16:49:09 +0300 Subject: [PATCH 21/33] Apply suggestions to Python code --- Lib/socket.py | 27 ++++++++++++++------------- Lib/ssl.py | 22 +++++++++++++++------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index cf6544d82636da..3073c012b19877 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -349,7 +349,8 @@ def makefile(self, mode="r", buffering=None, *, text.mode = mode return text - def _sendfile_zerocopy(self, zerocopy_func, giveup_err, file, offset=0, count=None): + def _sendfile_zerocopy(self, zerocopy_func, giveup_exc_type, file, + offset=0, count=None): """ Send a file using a zero-copy function. """ @@ -360,11 +361,11 @@ def _sendfile_zerocopy(self, zerocopy_func, giveup_err, file, offset=0, count=No try: fileno = file.fileno() except (AttributeError, io.UnsupportedOperation) as err: - raise giveup_err(err) # not a regular file + raise giveup_exc_type(err) # not a regular file try: fsize = os.fstat(fileno).st_size except OSError as err: - raise giveup_err(err) # not a regular file + raise giveup_exc_type(err) # not a regular file if not fsize: return 0 # empty file # Truncate to 1GiB to avoid OverflowError, see bpo-38319. @@ -406,7 +407,7 @@ def _sendfile_zerocopy(self, zerocopy_func, giveup_err, file, offset=0, count=No # one being 'file' is not a regular mmap(2)-like # file, in which case we'll fall back on using # plain send(). - raise giveup_err(err) + raise giveup_exc_type(err) raise err from None else: if sent == 0: @@ -418,17 +419,17 @@ def _sendfile_zerocopy(self, zerocopy_func, giveup_err, file, offset=0, count=No if total_sent > 0 and hasattr(file, 'seek'): file.seek(offset) - def _sendfile_use_sendfile(self, file, offset=0, count=None): - if not (sendfile := getattr(os, 'sendfile', None)): + if hasattr(os, 'sendfile'): + def _sendfile_use_sendfile(self, file, offset=0, count=None): + return self._sendfile_zerocopy( + partial(os.sendfile, self.fileno()), + _GiveupOnSendfile, + file, offset, count, + ) + else: + def _sendfile_use_sendfile(self, file, offset=0, count=None): raise _GiveupOnSendfile( "os.sendfile() not available on this platform") - return self._sendfile_zerocopy( - zerocopy_func=partial(sendfile, self.fileno()), - giveup_err=_GiveupOnSendfile, - file=file, - offset=offset, - count=count, - ) def _sendfile_use_send(self, file, offset=0, count=None): self._check_sendfile_params(file, offset, count) diff --git a/Lib/ssl.py b/Lib/ssl.py index 94af3dfed4f65f..7e84e5b1e61787 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1276,7 +1276,7 @@ def _sendfile_use_ssl_sendfile(self, file, offset=0, count=None): ) return self._sendfile_zerocopy( zerocopy_func=ssl_sendfile, - giveup_err=_GiveupOnSSLSendfile, + giveup_exc_type=_GiveupOnSSLSendfile, file=file, offset=offset, count=count, @@ -1288,12 +1288,20 @@ def sendfile(self, file, offset=0, count=None): """ if self._sslobj is None: return super().sendfile(file, offset, count) - if self._sslobj.uses_ktls_for_send(): - try: - return self._sendfile_use_ssl_sendfile(file, offset, count) - except _GiveupOnSSLSendfile: - pass - return self._sendfile_use_send(file, offset, count) + + if not self._sslobj.uses_ktls_for_send(): + return self._sendfile_use_send(file, offset, count) + + sendfile = getattr(self._sslobj, "sendfile", None) + if sendfile is None: + return self._sendfile_use_send(file, offset, count) + + try: + return self._sendfile_zerocopy( + sendfile, _GiveupOnSSLSendfile, file, offset, count, + ) + except _GiveupOnSSLSendfile: + return self._sendfile_use_send(file, offset, count) def recv(self, buflen=1024, flags=0): self._checkClosed() From 9a6a120c80de4f1a195052691181e2401d2fe0cf Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sat, 12 Apr 2025 17:14:10 +0300 Subject: [PATCH 22/33] Improve style of `_ssl__SSLSocket_sendfile_impl` --- Modules/_ssl.c | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 4829b78fbcd0be..49b6c27adf705e 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2530,7 +2530,6 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, Py_ssize_t retval; int sockstate; _PySSLError err; - int nonblocking; PySocketSockObject *sock = GET_SOCKET(self); PyTime_t timeout, deadline = 0; int has_timeout; @@ -2547,7 +2546,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, if (sock != NULL) { /* just in case the blocking state of the socket has been changed */ - nonblocking = (sock->sock_timeout >= 0); + int nonblocking = (sock->sock_timeout >= 0); BIO_set_nbio(SSL_get_rbio(self->ssl), nonblocking); BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking); } @@ -2559,18 +2558,19 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, } sockstate = PySSL_select(sock, 1, timeout); - if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PyExc_TimeoutError, - "The write operation timed out"); - goto error; - } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { - PyErr_SetString(get_state_sock(self)->PySSLErrorObject, - "Underlying socket has been closed."); - goto error; - } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) { - PyErr_SetString(get_state_sock(self)->PySSLErrorObject, - "Underlying socket too large for select()."); - goto error; + switch (sockstate) { + case SOCKET_HAS_TIMED_OUT: + PyErr_SetString(PyExc_TimeoutError, + "The write operation timed out"); + goto error; + case SOCKET_HAS_BEEN_CLOSED: + PyErr_SetString(get_state_sock(self)->PySSLErrorObject, + "Underlying socket has been closed."); + goto error; + case SOCKET_TOO_LARGE_FOR_SELECT: + PyErr_SetString(get_state_sock(self)->PySSLErrorObject, + "Underlying socket too large for select()."); + goto error; } do { @@ -2588,23 +2588,29 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, timeout = _PyDeadline_Get(deadline); } - if (err.ssl == SSL_ERROR_WANT_READ) { - sockstate = PySSL_select(sock, 0, timeout); - } else if (err.ssl == SSL_ERROR_WANT_WRITE) { - sockstate = PySSL_select(sock, 1, timeout); - } else { - sockstate = SOCKET_OPERATION_OK; + switch (err.ssl) { + case SSL_ERROR_WANT_READ: + sockstate = PySSL_select(sock, 0, timeout); + break; + case SSL_ERROR_WANT_WRITE: + sockstate = PySSL_select(sock, 1, timeout); + break; + default: + sockstate = SOCKET_OPERATION_OK; + break; } if (sockstate == SOCKET_HAS_TIMED_OUT) { PyErr_SetString(PyExc_TimeoutError, "The sendfile operation timed out"); goto error; - } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { + } + else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { PyErr_SetString(get_state_sock(self)->PySSLErrorObject, "Underlying socket has been closed."); goto error; - } else if (sockstate == SOCKET_IS_NONBLOCKING) { + } + else if (sockstate == SOCKET_IS_NONBLOCKING) { break; } } while (err.ssl == SSL_ERROR_WANT_READ || From 05a0c6c15dd8bb3ea84eb70840cf9e37619417e1 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sun, 13 Apr 2025 18:41:45 +0300 Subject: [PATCH 23/33] Drop `_sendfile_use_ssl_sendfile` --- Lib/ssl.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index 7e84e5b1e61787..b2b666ec4fdff2 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1269,19 +1269,6 @@ def sendall(self, data, flags=0): else: return super().sendall(data, flags) - def _sendfile_use_ssl_sendfile(self, file, offset=0, count=None): - if not (ssl_sendfile := getattr(self._sslobj, "sendfile", None)): - raise _GiveupOnSSLSendfile( - "SSL_sendfile() not available on this platform", - ) - return self._sendfile_zerocopy( - zerocopy_func=ssl_sendfile, - giveup_exc_type=_GiveupOnSSLSendfile, - file=file, - offset=offset, - count=count, - ) - def sendfile(self, file, offset=0, count=None): """Send a file, possibly by using an efficient sendfile() call if the system supports it. Return the total number of bytes sent. From 31ed52d5ed4030ec71821f94df5184571640a29d Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 18 Apr 2025 16:27:11 +0300 Subject: [PATCH 24/33] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/ssl.rst | 2 +- .../2023-03-13-22-51-40.gh-issue-99813.40TV02.rst | 2 +- Modules/_ssl.c | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index d98d6646c7c3ad..99eb7707946925 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1108,7 +1108,7 @@ SSL Sockets zero-length data no longer fails with a protocol violation error. .. versionchanged:: next - Python now uses ``SSL_sendifle`` internally when it is possible. The + Python now uses ``SSL_sendfile`` internally when possible. The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. diff --git a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst index 5d5287e027ee20..8ee459dc420647 100644 --- a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst +++ b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst @@ -1,4 +1,4 @@ Python now uses ``SSL_sendfile`` internally when it is possible (see -:data:`~ssl.OP_ENABLE_KTLS`.) The function sends a file more efficiently +:data:`~ssl.OP_ENABLE_KTLS`). The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. Patch by Illia Volochii. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b93966caa2d4e9..5ee23119070364 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -75,11 +75,11 @@ #ifdef BIO_get_ktls_send -#ifdef MS_WINDOWS - typedef long long Py_off_t; -#else - typedef off_t Py_off_t; -#endif +# ifdef MS_WINDOWS +typedef long long Py_off_t; +# else +typedef off_t Py_off_t; +# endif static int Py_off_t_converter(PyObject *arg, void *addr) From ada3f3063ddaf2a67646d4a04a71ec0391b55bef Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 18 Apr 2025 18:09:37 +0300 Subject: [PATCH 25/33] Improve kTLS checks for older OpenSSL https://github.com/openssl/openssl/commit/524bac570702a79366b85ff1f66e07d3e002370c --- Modules/_ssl.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 5ee23119070364..13653a760bcdc5 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2479,7 +2479,14 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) { #ifdef BIO_get_ktls_send int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); - return PyBool_FromLong(uses); + // BIO_get_ktls_send() returns 1 if kTLS is used and 0 if not. + // Also, it returns -1 for failure before OpenSSL 3.0.4. + if (uses == 1) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } #else Py_RETURN_FALSE; #endif @@ -2497,7 +2504,14 @@ _ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) { #ifdef BIO_get_ktls_recv int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); - return PyBool_FromLong(uses); + // BIO_get_ktls_recv() returns 1 if kTLS is used and 0 if not. + // Also, it returns -1 for failure before OpenSSL 3.0.4. + if (uses == 1) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } #else Py_RETURN_FALSE; #endif From 33396732a9711278af3d90f1742ec3d6b1502786 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 28 Apr 2025 23:45:52 +0300 Subject: [PATCH 26/33] Rename `uses_ktls_for_read` to `uses_ktls_for_recv` --- Modules/_ssl.c | 8 ++++---- Modules/clinic/_ssl.c.h | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index f93d7b393e45f2..822de7ab7e76dc 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2494,14 +2494,14 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) } /*[clinic input] -_ssl._SSLSocket.uses_ktls_for_read +_ssl._SSLSocket.uses_ktls_for_recv Check if the Kernel TLS data-path is used for receiving. [clinic start generated code]*/ static PyObject * -_ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self) -/*[clinic end generated code: output=140a75033c8316a6 input=0296846b94a57932]*/ +_ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self) +/*[clinic end generated code: output=ce38b00317a1f681 input=fc237448ad8cfe18]*/ { #ifdef BIO_get_ktls_recv int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); @@ -3233,7 +3233,7 @@ static PyGetSetDef ssl_getsetlist[] = { static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_DO_HANDSHAKE_METHODDEF _SSL__SSLSOCKET_USES_KTLS_FOR_SEND_METHODDEF - _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF + _SSL__SSLSOCKET_USES_KTLS_FOR_RECV_METHODDEF _SSL__SSLSOCKET_SENDFILE_METHODDEF _SSL__SSLSOCKET_WRITE_METHODDEF _SSL__SSLSOCKET_READ_METHODDEF diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 2e7f77deb27a10..ebb5771b0dafcd 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -461,22 +461,22 @@ _ssl__SSLSocket_uses_ktls_for_send(PyObject *self, PyObject *Py_UNUSED(ignored)) return _ssl__SSLSocket_uses_ktls_for_send_impl((PySSLSocket *)self); } -PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_read__doc__, -"uses_ktls_for_read($self, /)\n" +PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_recv__doc__, +"uses_ktls_for_recv($self, /)\n" "--\n" "\n" "Check if the Kernel TLS data-path is used for receiving."); -#define _SSL__SSLSOCKET_USES_KTLS_FOR_READ_METHODDEF \ - {"uses_ktls_for_read", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_read, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_read__doc__}, +#define _SSL__SSLSOCKET_USES_KTLS_FOR_RECV_METHODDEF \ + {"uses_ktls_for_recv", (PyCFunction)_ssl__SSLSocket_uses_ktls_for_recv, METH_NOARGS, _ssl__SSLSocket_uses_ktls_for_recv__doc__}, static PyObject * -_ssl__SSLSocket_uses_ktls_for_read_impl(PySSLSocket *self); +_ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self); static PyObject * -_ssl__SSLSocket_uses_ktls_for_read(PyObject *self, PyObject *Py_UNUSED(ignored)) +_ssl__SSLSocket_uses_ktls_for_recv(PyObject *self, PyObject *Py_UNUSED(ignored)) { - return _ssl__SSLSocket_uses_ktls_for_read_impl((PySSLSocket *)self); + return _ssl__SSLSocket_uses_ktls_for_recv_impl((PySSLSocket *)self); } #if defined(BIO_get_ktls_send) @@ -3002,4 +3002,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=26e56272e97fa71f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=2a1a9b4a90b86168 input=a9049054013a1b77]*/ From ee573add2a92226c9f113c5dd3743f326f80f6e3 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 28 Apr 2025 23:47:13 +0300 Subject: [PATCH 27/33] Apply PEP 7 --- Modules/_ssl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 822de7ab7e76dc..14266a3546c00b 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2628,8 +2628,8 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, else if (sockstate == SOCKET_IS_NONBLOCKING) { break; } - } while (err.ssl == SSL_ERROR_WANT_READ || - err.ssl == SSL_ERROR_WANT_WRITE); + } while (err.ssl == SSL_ERROR_WANT_READ + || err.ssl == SSL_ERROR_WANT_WRITE); if (err.ssl == SSL_ERROR_SSL && ERR_GET_REASON(ERR_peek_error()) == SSL_R_UNINITIALIZED) From 6795f62b95bfe449e425cc2546856bdc705af15c Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Mon, 28 Apr 2025 23:52:49 +0300 Subject: [PATCH 28/33] Use an alternative method of returning booleans --- Modules/_ssl.c | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 14266a3546c00b..38c383899fc497 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2482,14 +2482,9 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); // BIO_get_ktls_send() returns 1 if kTLS is used and 0 if not. // Also, it returns -1 for failure before OpenSSL 3.0.4. - if (uses == 1) { - Py_RETURN_TRUE; - } - else { - Py_RETURN_FALSE; - } + return Py_NewRef(uses == 1 ? Py_True : Py_False); #else - Py_RETURN_FALSE; + return Py_NewRef(Py_False); #endif } @@ -2507,14 +2502,9 @@ _ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self) int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); // BIO_get_ktls_recv() returns 1 if kTLS is used and 0 if not. // Also, it returns -1 for failure before OpenSSL 3.0.4. - if (uses == 1) { - Py_RETURN_TRUE; - } - else { - Py_RETURN_FALSE; - } + return Py_NewRef(uses == 1 ? Py_True : Py_False); #else - Py_RETURN_FALSE; + return Py_NewRef(Py_False); #endif } From 6a35ac6692e78a106949f9fe3083cb23db99467e Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 2 May 2025 17:05:03 +0300 Subject: [PATCH 29/33] Mark new functions with `critical_section` --- Modules/_ssl.c | 6 ++++-- Modules/clinic/_ssl.c.h | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 38c383899fc497..b017f46cfcf691 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2469,6 +2469,7 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout) } /*[clinic input] +@critical_section _ssl._SSLSocket.uses_ktls_for_send Check if the Kernel TLS data-path is used for sending. @@ -2476,7 +2477,7 @@ Check if the Kernel TLS data-path is used for sending. static PyObject * _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) -/*[clinic end generated code: output=f9d95fbefceb5068 input=604d98b67c65e8a7]*/ +/*[clinic end generated code: output=f9d95fbefceb5068 input=8d1ce4a131190e6b]*/ { #ifdef BIO_get_ktls_send int uses = BIO_get_ktls_send(SSL_get_wbio(self->ssl)); @@ -2489,6 +2490,7 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) } /*[clinic input] +@critical_section _ssl._SSLSocket.uses_ktls_for_recv Check if the Kernel TLS data-path is used for receiving. @@ -2496,7 +2498,7 @@ Check if the Kernel TLS data-path is used for receiving. static PyObject * _ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self) -/*[clinic end generated code: output=ce38b00317a1f681 input=fc237448ad8cfe18]*/ +/*[clinic end generated code: output=ce38b00317a1f681 input=a13778a924fc7d44]*/ { #ifdef BIO_get_ktls_recv int uses = BIO_get_ktls_recv(SSL_get_rbio(self->ssl)); diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index ebb5771b0dafcd..7027d87379283d 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -458,7 +458,13 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self); static PyObject * _ssl__SSLSocket_uses_ktls_for_send(PyObject *self, PyObject *Py_UNUSED(ignored)) { - return _ssl__SSLSocket_uses_ktls_for_send_impl((PySSLSocket *)self); + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLSocket_uses_ktls_for_send_impl((PySSLSocket *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; } PyDoc_STRVAR(_ssl__SSLSocket_uses_ktls_for_recv__doc__, @@ -476,7 +482,13 @@ _ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self); static PyObject * _ssl__SSLSocket_uses_ktls_for_recv(PyObject *self, PyObject *Py_UNUSED(ignored)) { - return _ssl__SSLSocket_uses_ktls_for_recv_impl((PySSLSocket *)self); + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLSocket_uses_ktls_for_recv_impl((PySSLSocket *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; } #if defined(BIO_get_ktls_send) @@ -3002,4 +3014,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=2a1a9b4a90b86168 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=1adc3780d8ca682a input=a9049054013a1b77]*/ From 9dffdbde5a47abc0c3b719442fd87a563ee8bc6a Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 2 May 2025 17:08:38 +0300 Subject: [PATCH 30/33] Merge two if blocks --- Modules/_ssl.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b017f46cfcf691..e686409b11b564 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2549,9 +2549,6 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, return NULL; } Py_INCREF(sock); - } - - if (sock != NULL) { /* just in case the blocking state of the socket has been changed */ int nonblocking = (sock->sock_timeout >= 0); BIO_set_nbio(SSL_get_rbio(self->ssl), nonblocking); From 19d57468ba4434828da3105031c0223a1aeb84ba Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 2 May 2025 17:13:37 +0300 Subject: [PATCH 31/33] Reword docs --- Doc/library/ssl.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 99eb7707946925..138a5c4aa67f3e 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1071,8 +1071,8 @@ SSL Sockets (but passing a non-zero ``flags`` argument is not allowed) - :meth:`~socket.socket.send`, :meth:`~socket.socket.sendall` (with the same limitation) - - :meth:`~socket.socket.sendfile` (but it may be high-performant only when - the kernel TLS is enabled (see :data:`~ssl.OP_ENABLE_KTLS`) or when a + - :meth:`~socket.socket.sendfile` (it may be high-performant only when + the kernel TLS is enabled by setting :data:`~ssl.OP_ENABLE_KTLS` or when a socket is plain-text, else :meth:`~socket.socket.send` will be used) - :meth:`~socket.socket.shutdown` From ef3744e5dcff021cc5806fb535aa736f0bf20156 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 2 May 2025 17:16:11 +0300 Subject: [PATCH 32/33] Update the news entry Co-authored-by: Peter Bierma --- .../next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst index 8ee459dc420647..c511c63021442b 100644 --- a/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst +++ b/Misc/NEWS.d/next/Library/2023-03-13-22-51-40.gh-issue-99813.40TV02.rst @@ -1,4 +1,4 @@ -Python now uses ``SSL_sendfile`` internally when it is possible (see +:mod:`ssl` now uses ``SSL_sendfile`` internally when it is possible (see :data:`~ssl.OP_ENABLE_KTLS`). The function sends a file more efficiently because it performs TLS encryption in the kernel to avoid additional context switches. Patch by Illia Volochii. From e64f329dae802fa80eb236f626ff547518962a96 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Fri, 2 May 2025 17:21:48 +0300 Subject: [PATCH 33/33] Use `Py_RETURN_FALSE` again --- Modules/_ssl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index e686409b11b564..8cd36d2cb9fa5f 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2485,7 +2485,7 @@ _ssl__SSLSocket_uses_ktls_for_send_impl(PySSLSocket *self) // Also, it returns -1 for failure before OpenSSL 3.0.4. return Py_NewRef(uses == 1 ? Py_True : Py_False); #else - return Py_NewRef(Py_False); + Py_RETURN_FALSE; #endif } @@ -2506,7 +2506,7 @@ _ssl__SSLSocket_uses_ktls_for_recv_impl(PySSLSocket *self) // Also, it returns -1 for failure before OpenSSL 3.0.4. return Py_NewRef(uses == 1 ? Py_True : Py_False); #else - return Py_NewRef(Py_False); + Py_RETURN_FALSE; #endif }