diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 92ff5131a58e9d..97975db3bf49fb 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2843,6 +2843,36 @@ def close(self): def stop(self): self.active = False +class TestEOFServer(threading.Thread): + def __init__(self): + super().__init__() + self.listening = threading.Event() + self.address = None + + def run(self): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(CERTFILE) + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with server_sock: + server_sock.settimeout(support.SHORT_TIMEOUT) + server_sock.bind((HOST, 0)) + server_sock.listen(5) + + self.address = server_sock.getsockname() + self.listening.set() + + sock, addr = server_sock.accept() + sslconn = context.wrap_socket(sock, server_side=True) + with sslconn: + request = b'' + while chunk := sslconn.recv(1024): + request += chunk + if b'\n' in chunk: + break + + sslconn.sendall(b'server\n') + sslconn.shutdown(socket.SHUT_WR) + class AsyncoreEchoServer(threading.Thread): # this one's based on asyncore.dispatcher @@ -5001,6 +5031,58 @@ def background(sock): if cm.exc_value is not None: raise cm.exc_value + def test_got_eof(self): + # gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL + # versions on calling methods after EOF (after the first SSLEOFError). + + server = TestEOFServer() + server.start() + if not server.listening.wait(support.SHORT_TIMEOUT): + raise RuntimeError("server took too long") + self.addCleanup(server.join) + + context = ssl.create_default_context(cafile=CERTFILE) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(support.SHORT_TIMEOUT) + sock.connect(server.address) + sslsock = context.wrap_socket(sock, server_hostname='localhost') + with sslsock: + sslsock.sendall(b'client\n') + # test the _ssl._SSLSocket object, not ssl.SSLSocket + sslobj = sslsock._sslobj + + data = sslobj.read(1024) + self.assertEqual(data, b'server\n') + + # The second read gets EOF error and sets got_eof_error to 1 + with self.assertRaises(ssl.SSLEOFError): + sslobj.read(1024) + + # Following read(), sendfile(), write() and do_handshake() calls + # must raise SSLEOFError + with self.assertRaises(ssl.SSLEOFError): + # The _SSLSocket remembers the previous EOF error + # and raises again SSLEOFError + sslobj.read(1024) + if hasattr(sslobj, 'sendfile'): + with open(__file__, "rb") as fp: + with self.assertRaises(ssl.SSLEOFError): + sslobj.sendfile(fp.fileno(), 0, 1) + with self.assertRaises(ssl.SSLEOFError): + sslobj.write(b'client2\n') + with self.assertRaises(ssl.SSLEOFError): + sslsock.do_handshake() + + self.assertEqual(sslsock.pending(), 0) + try: + sslsock.shutdown(socket.SHUT_WR) + except OSError as exc: + self.assertEqual(exc.errno, errno.ENOTCONN) + else: + # On Windows and on OpenSSL 1.1.1, shutdown() doesn't + # raise an error + pass + @unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA, "Test needs TLS 1.3 PHA") diff --git a/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst b/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst new file mode 100644 index 00000000000000..e1f308df5a678e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst @@ -0,0 +1,7 @@ +:mod:`ssl`: Update :class:`ssl.SSLSocket` and :class:`ssl.SSLObject` for +OpenSSL 4. The classes now remember if they get a :exc:`ssl.SSLEOFError`. In this +case, following :meth:`~ssl.SSLSocket.read`, :meth:`!sendfile`, +:meth:`~ssl.SSLSocket.write`, and :meth:`~ssl.SSLSocket.do_handshake` calls +raise :exc:`ssl.SSLEOFError` without calling the underlying OpenSSL function. +Thanks to that, :class:`ssl.SSLSocket` behaves the same on all OpenSSL versions +on EOF. Patch by Victor Stinner. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ea8a6d3fc1daca..3224ca7d0f93b9 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -377,6 +377,16 @@ typedef struct { enum py_ssl_server_or_client socket_type; PyObject *owner; /* weakref to Python level "owner" passed to servername callback */ PyObject *server_hostname; + // gh-148292: If non-zero, read(), sendfile(), write() and do_handshake() + // methods raise SSLEOFError without calling the underlying OpenSSL + // function. Set to 1 on PY_SSL_ERROR_EOF error. + // + // On OpenSSL 4, if SSL_read_ex() fails with + // SSL_R_UNEXPECTED_EOF_WHILE_READING, the following SSL_read_ex() call + // fails with a generic protocol error (ERR_peek_last_error() returns 0). + // Use got_eof_error to have the same behavior on OpenSSL 4 and newer and + // on OpenSSL 3 and older. + int got_eof_error; } PySSLSocket; #define PySSLSocket_CAST(op) ((PySSLSocket *)(op)) @@ -504,6 +514,10 @@ fill_and_set_sslerror(_sslmodulestate *state, PyObject *init_value, *msg, *key; PyUnicodeWriter *writer = NULL; + if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) { + sslsock->got_eof_error = 1; + } + if (errcode != 0) { int lib, reason; @@ -649,6 +663,18 @@ fill_and_set_sslerror(_sslmodulestate *state, PyUnicodeWriter_Discard(writer); } + +static void +set_eof_error(PySSLSocket *sslsock) +{ + _sslmodulestate *state = get_state_sock(sslsock); + fill_and_set_sslerror(state, sslsock, state->PySSLEOFErrorObject, + PY_SSL_ERROR_EOF, + "EOF occurred in violation of protocol", + __LINE__, 0); +} + + // Set the appropriate SSL error exception. // err - error information from SSL and libc // exc - if not NULL, an exception from _debughelpers.c callback to be chained @@ -923,6 +949,7 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, self->shutdown_seen_zero = 0; self->owner = NULL; self->server_hostname = NULL; + self->got_eof_error = 0; /* Make sure the SSL error state is initialized */ ERR_clear_error(); @@ -1053,6 +1080,11 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self) return NULL; } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); if (has_timeout) { @@ -2638,6 +2670,11 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, return NULL; } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); if (has_timeout) { @@ -2765,6 +2802,11 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b) return NULL; } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); if (has_timeout) { @@ -2905,6 +2947,11 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len, return NULL; } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + if (!group_right_1) { if (len == 0) { Py_XDECREF(sock);