mirror of
https://github.com/python/cpython.git
synced 2026-06-04 16:50:51 +00:00
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:
parent
bc7c102f34
commit
7b7fa3f9bf
3 changed files with 136 additions and 0 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue