Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment thread
vstinner marked this conversation as resolved.
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
Expand Down Expand Up @@ -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()
Comment thread
vstinner marked this conversation as resolved.
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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading