https://github.com/python/cpython/commit/7b7fa3f9bf3d7cdf3eb669d02b386e05b39c402a
commit: 7b7fa3f9bf3d7cdf3eb669d02b386e05b39c402a
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-05-04T13:52:57+02:00
summary:

gh-148292: Update _ssl._SSLSocket for OpenSSL 4 (#149102)

The _SSLSocket object now remembers if it gets an EOF error. In this
case, read(), sendfile(), write() and do_handshake method calls fail
with SSLEOFError without calling the underlying OpenSSL function.

Co-authored-by: Gregory P. Smith <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst
M Lib/test/test_ssl.py
M Modules/_ssl.c

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);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to