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 <greg@krypto.org>
This commit is contained in:
Victor Stinner 2026-05-04 13:52:57 +02:00 committed by GitHub
parent bc7c102f34
commit 7b7fa3f9bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 136 additions and 0 deletions

View file

@ -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")

View file

@ -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.

View file

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