Skip to content

Commit bd3a782

Browse files
vstinnergpshead
andcommitted
pythongh-148292: Update _ssl._SSLSocket for OpenSSL 4 (python#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> (cherry picked from commit 7b7fa3f)
1 parent c5e4ae0 commit bd3a782

3 files changed

Lines changed: 134 additions & 0 deletions

File tree

Lib/test/test_ssl.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,6 +2711,36 @@ def close(self):
27112711
def stop(self):
27122712
self.active = False
27132713

2714+
class TestEOFServer(threading.Thread):
2715+
def __init__(self):
2716+
super().__init__()
2717+
self.listening = threading.Event()
2718+
self.address = None
2719+
2720+
def run(self):
2721+
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
2722+
context.load_cert_chain(CERTFILE)
2723+
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2724+
with server_sock:
2725+
server_sock.settimeout(support.SHORT_TIMEOUT)
2726+
server_sock.bind((HOST, 0))
2727+
server_sock.listen(5)
2728+
2729+
self.address = server_sock.getsockname()
2730+
self.listening.set()
2731+
2732+
sock, addr = server_sock.accept()
2733+
sslconn = context.wrap_socket(sock, server_side=True)
2734+
with sslconn:
2735+
request = b''
2736+
while chunk := sslconn.recv(1024):
2737+
request += chunk
2738+
if b'\n' in chunk:
2739+
break
2740+
2741+
sslconn.sendall(b'server\n')
2742+
sslconn.shutdown(socket.SHUT_WR)
2743+
27142744
class AsyncoreEchoServer(threading.Thread):
27152745

27162746
# this one's based on asyncore.dispatcher
@@ -4747,6 +4777,58 @@ def background(sock):
47474777
if cm.exc_value is not None:
47484778
raise cm.exc_value
47494779

4780+
def test_got_eof(self):
4781+
# gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL
4782+
# versions on calling methods after EOF (after the first SSLEOFError).
4783+
4784+
server = TestEOFServer()
4785+
server.start()
4786+
if not server.listening.wait(support.SHORT_TIMEOUT):
4787+
raise RuntimeError("server took too long")
4788+
self.addCleanup(server.join)
4789+
4790+
context = ssl.create_default_context(cafile=CERTFILE)
4791+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4792+
sock.settimeout(support.SHORT_TIMEOUT)
4793+
sock.connect(server.address)
4794+
sslsock = context.wrap_socket(sock, server_hostname='localhost')
4795+
with sslsock:
4796+
sslsock.sendall(b'client\n')
4797+
# test the _ssl._SSLSocket object, not ssl.SSLSocket
4798+
sslobj = sslsock._sslobj
4799+
4800+
data = sslobj.read(1024)
4801+
self.assertEqual(data, b'server\n')
4802+
4803+
# The second read gets EOF error and sets got_eof_error to 1
4804+
with self.assertRaises(ssl.SSLEOFError):
4805+
sslobj.read(1024)
4806+
4807+
# Following read(), sendfile(), write() and do_handshake() calls
4808+
# must raise SSLEOFError
4809+
with self.assertRaises(ssl.SSLEOFError):
4810+
# The _SSLSocket remembers the previous EOF error
4811+
# and raises again SSLEOFError
4812+
sslobj.read(1024)
4813+
if hasattr(sslobj, 'sendfile'):
4814+
with open(__file__, "rb") as fp:
4815+
with self.assertRaises(ssl.SSLEOFError):
4816+
sslobj.sendfile(fp.fileno(), 0, 1)
4817+
with self.assertRaises(ssl.SSLEOFError):
4818+
sslobj.write(b'client2\n')
4819+
with self.assertRaises(ssl.SSLEOFError):
4820+
sslsock.do_handshake()
4821+
4822+
self.assertEqual(sslsock.pending(), 0)
4823+
try:
4824+
sslsock.shutdown(socket.SHUT_WR)
4825+
except OSError as exc:
4826+
self.assertEqual(exc.errno, errno.ENOTCONN)
4827+
else:
4828+
# On Windows and on OpenSSL 1.1.1, shutdown() doesn't
4829+
# raise an error
4830+
pass
4831+
47504832

47514833
@unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA,
47524834
"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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ typedef struct {
352352
* and shutdown methods check for chained exceptions.
353353
*/
354354
PyObject *exc;
355+
// gh-148292: If non-zero, read(), sendfile(), write() and do_handshake()
356+
// methods raise SSLEOFError without calling the underlying OpenSSL
357+
// function. Set to 1 on PY_SSL_ERROR_EOF error.
358+
//
359+
// On OpenSSL 4, if SSL_read_ex() fails with
360+
// SSL_R_UNEXPECTED_EOF_WHILE_READING, the following SSL_read_ex() call
361+
// fails with a generic protocol error (ERR_peek_last_error() returns 0).
362+
// Use got_eof_error to have the same behavior on OpenSSL 4 and newer and
363+
// on OpenSSL 3 and older.
364+
int got_eof_error;
355365
} PySSLSocket;
356366

357367
#define PySSLSocket_CAST(op) ((PySSLSocket *)(op))
@@ -499,6 +509,10 @@ fill_and_set_sslerror(_sslmodulestate *state,
499509
PyObject *init_value, *msg, *key;
500510
PyUnicodeWriter *writer = NULL;
501511

512+
if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) {
513+
sslsock->got_eof_error = 1;
514+
}
515+
502516
if (errcode != 0) {
503517
int lib, reason;
504518

@@ -654,6 +668,21 @@ PySSL_ChainExceptions(PySSLSocket *sslsock) {
654668
return -1;
655669
}
656670

671+
672+
static void
673+
set_eof_error(PySSLSocket *sslsock)
674+
{
675+
_sslmodulestate *state = get_state_sock(sslsock);
676+
fill_and_set_sslerror(state, sslsock, state->PySSLEOFErrorObject,
677+
PY_SSL_ERROR_EOF,
678+
"EOF occurred in violation of protocol",
679+
__LINE__, 0);
680+
}
681+
682+
683+
// Set the appropriate SSL error exception.
684+
// err - error information from SSL and libc
685+
// exc - if not NULL, an exception from _debughelpers.c callback to be chained
657686
static PyObject *
658687
PySSL_SetError(PySSLSocket *sslsock, const char *filename, int lineno)
659688
{
@@ -901,6 +930,7 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
901930
self->server_hostname = NULL;
902931
self->err = err;
903932
self->exc = NULL;
933+
self->got_eof_error = 0;
904934

905935
/* Make sure the SSL error state is initialized */
906936
ERR_clear_error();
@@ -1041,6 +1071,11 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self)
10411071
BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking);
10421072
}
10431073

1074+
if (self->got_eof_error) {
1075+
set_eof_error(self);
1076+
goto error;
1077+
}
1078+
10441079
timeout = GET_SOCKET_TIMEOUT(sock);
10451080
has_timeout = (timeout > 0);
10461081
if (has_timeout) {
@@ -2504,6 +2539,11 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b)
25042539
BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking);
25052540
}
25062541

2542+
if (self->got_eof_error) {
2543+
set_eof_error(self);
2544+
goto error;
2545+
}
2546+
25072547
timeout = GET_SOCKET_TIMEOUT(sock);
25082548
has_timeout = (timeout > 0);
25092549
if (has_timeout) {
@@ -2644,6 +2684,11 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len,
26442684
Py_INCREF(sock);
26452685
}
26462686

2687+
if (self->got_eof_error) {
2688+
set_eof_error(self);
2689+
goto error;
2690+
}
2691+
26472692
if (!group_right_1) {
26482693
dest = PyBytes_FromStringAndSize(NULL, len);
26492694
if (dest == NULL)

0 commit comments

Comments
 (0)