Skip to content

Commit 7b7fa3f

Browse files
vstinnergpshead
andauthored
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>
1 parent bc7c102 commit 7b7fa3f

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lib/test/test_ssl.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2843,6 +2843,36 @@ def close(self):
28432843
def stop(self):
28442844
self.active = False
28452845

2846+
class TestEOFServer(threading.Thread):
2847+
def __init__(self):
2848+
super().__init__()
2849+
self.listening = threading.Event()
2850+
self.address = None
2851+
2852+
def run(self):
2853+
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
2854+
context.load_cert_chain(CERTFILE)
2855+
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2856+
with server_sock:
2857+
server_sock.settimeout(support.SHORT_TIMEOUT)
2858+
server_sock.bind((HOST, 0))
2859+
server_sock.listen(5)
2860+
2861+
self.address = server_sock.getsockname()
2862+
self.listening.set()
2863+
2864+
sock, addr = server_sock.accept()
2865+
sslconn = context.wrap_socket(sock, server_side=True)
2866+
with sslconn:
2867+
request = b''
2868+
while chunk := sslconn.recv(1024):
2869+
request += chunk
2870+
if b'\n' in chunk:
2871+
break
2872+
2873+
sslconn.sendall(b'server\n')
2874+
sslconn.shutdown(socket.SHUT_WR)
2875+
28462876
class AsyncoreEchoServer(threading.Thread):
28472877

28482878
# this one's based on asyncore.dispatcher
@@ -5001,6 +5031,58 @@ def background(sock):
50015031
if cm.exc_value is not None:
50025032
raise cm.exc_value
50035033

5034+
def test_got_eof(self):
5035+
# gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL
5036+
# versions on calling methods after EOF (after the first SSLEOFError).
5037+
5038+
server = TestEOFServer()
5039+
server.start()
5040+
if not server.listening.wait(support.SHORT_TIMEOUT):
5041+
raise RuntimeError("server took too long")
5042+
self.addCleanup(server.join)
5043+
5044+
context = ssl.create_default_context(cafile=CERTFILE)
5045+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
5046+
sock.settimeout(support.SHORT_TIMEOUT)
5047+
sock.connect(server.address)
5048+
sslsock = context.wrap_socket(sock, server_hostname='localhost')
5049+
with sslsock:
5050+
sslsock.sendall(b'client\n')
5051+
# test the _ssl._SSLSocket object, not ssl.SSLSocket
5052+
sslobj = sslsock._sslobj
5053+
5054+
data = sslobj.read(1024)
5055+
self.assertEqual(data, b'server\n')
5056+
5057+
# The second read gets EOF error and sets got_eof_error to 1
5058+
with self.assertRaises(ssl.SSLEOFError):
5059+
sslobj.read(1024)
5060+
5061+
# Following read(), sendfile(), write() and do_handshake() calls
5062+
# must raise SSLEOFError
5063+
with self.assertRaises(ssl.SSLEOFError):
5064+
# The _SSLSocket remembers the previous EOF error
5065+
# and raises again SSLEOFError
5066+
sslobj.read(1024)
5067+
if hasattr(sslobj, 'sendfile'):
5068+
with open(__file__, "rb") as fp:
5069+
with self.assertRaises(ssl.SSLEOFError):
5070+
sslobj.sendfile(fp.fileno(), 0, 1)
5071+
with self.assertRaises(ssl.SSLEOFError):
5072+
sslobj.write(b'client2\n')
5073+
with self.assertRaises(ssl.SSLEOFError):
5074+
sslsock.do_handshake()
5075+
5076+
self.assertEqual(sslsock.pending(), 0)
5077+
try:
5078+
sslsock.shutdown(socket.SHUT_WR)
5079+
except OSError as exc:
5080+
self.assertEqual(exc.errno, errno.ENOTCONN)
5081+
else:
5082+
# On Windows and on OpenSSL 1.1.1, shutdown() doesn't
5083+
# raise an error
5084+
pass
5085+
50045086

50055087
@unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA,
50065088
"Test needs TLS 1.3 PHA")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:mod:`ssl`: Update :class:`ssl.SSLSocket` and :class:`ssl.SSLObject` for
2+
OpenSSL 4. The classes now remember if they get a :exc:`ssl.SSLEOFError`. In this
3+
case, following :meth:`~ssl.SSLSocket.read`, :meth:`!sendfile`,
4+
:meth:`~ssl.SSLSocket.write`, and :meth:`~ssl.SSLSocket.do_handshake` calls
5+
raise :exc:`ssl.SSLEOFError` without calling the underlying OpenSSL function.
6+
Thanks to that, :class:`ssl.SSLSocket` behaves the same on all OpenSSL versions
7+
on EOF. Patch by Victor Stinner.

Modules/_ssl.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ typedef struct {
377377
enum py_ssl_server_or_client socket_type;
378378
PyObject *owner; /* weakref to Python level "owner" passed to servername callback */
379379
PyObject *server_hostname;
380+
// gh-148292: If non-zero, read(), sendfile(), write() and do_handshake()
381+
// methods raise SSLEOFError without calling the underlying OpenSSL
382+
// function. Set to 1 on PY_SSL_ERROR_EOF error.
383+
//
384+
// On OpenSSL 4, if SSL_read_ex() fails with
385+
// SSL_R_UNEXPECTED_EOF_WHILE_READING, the following SSL_read_ex() call
386+
// fails with a generic protocol error (ERR_peek_last_error() returns 0).
387+
// Use got_eof_error to have the same behavior on OpenSSL 4 and newer and
388+
// on OpenSSL 3 and older.
389+
int got_eof_error;
380390
} PySSLSocket;
381391

382392
#define PySSLSocket_CAST(op) ((PySSLSocket *)(op))
@@ -504,6 +514,10 @@ fill_and_set_sslerror(_sslmodulestate *state,
504514
PyObject *init_value, *msg, *key;
505515
PyUnicodeWriter *writer = NULL;
506516

517+
if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) {
518+
sslsock->got_eof_error = 1;
519+
}
520+
507521
if (errcode != 0) {
508522
int lib, reason;
509523

@@ -649,6 +663,18 @@ fill_and_set_sslerror(_sslmodulestate *state,
649663
PyUnicodeWriter_Discard(writer);
650664
}
651665

666+
667+
static void
668+
set_eof_error(PySSLSocket *sslsock)
669+
{
670+
_sslmodulestate *state = get_state_sock(sslsock);
671+
fill_and_set_sslerror(state, sslsock, state->PySSLEOFErrorObject,
672+
PY_SSL_ERROR_EOF,
673+
"EOF occurred in violation of protocol",
674+
__LINE__, 0);
675+
}
676+
677+
652678
// Set the appropriate SSL error exception.
653679
// err - error information from SSL and libc
654680
// exc - if not NULL, an exception from _debughelpers.c callback to be chained
@@ -923,6 +949,7 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
923949
self->shutdown_seen_zero = 0;
924950
self->owner = NULL;
925951
self->server_hostname = NULL;
952+
self->got_eof_error = 0;
926953

927954
/* Make sure the SSL error state is initialized */
928955
ERR_clear_error();
@@ -1053,6 +1080,11 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self)
10531080
return NULL;
10541081
}
10551082

1083+
if (self->got_eof_error) {
1084+
set_eof_error(self);
1085+
goto error;
1086+
}
1087+
10561088
timeout = GET_SOCKET_TIMEOUT(sock);
10571089
has_timeout = (timeout > 0);
10581090
if (has_timeout) {
@@ -2638,6 +2670,11 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset,
26382670
return NULL;
26392671
}
26402672

2673+
if (self->got_eof_error) {
2674+
set_eof_error(self);
2675+
goto error;
2676+
}
2677+
26412678
timeout = GET_SOCKET_TIMEOUT(sock);
26422679
has_timeout = (timeout > 0);
26432680
if (has_timeout) {
@@ -2765,6 +2802,11 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b)
27652802
return NULL;
27662803
}
27672804

2805+
if (self->got_eof_error) {
2806+
set_eof_error(self);
2807+
goto error;
2808+
}
2809+
27682810
timeout = GET_SOCKET_TIMEOUT(sock);
27692811
has_timeout = (timeout > 0);
27702812
if (has_timeout) {
@@ -2905,6 +2947,11 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len,
29052947
return NULL;
29062948
}
29072949

2950+
if (self->got_eof_error) {
2951+
set_eof_error(self);
2952+
goto error;
2953+
}
2954+
29082955
if (!group_right_1) {
29092956
if (len == 0) {
29102957
Py_XDECREF(sock);

0 commit comments

Comments
 (0)