Skip to content

Commit e51acb3

Browse files
[3.14] gh-134698: Hold a lock when the thread state is detached in ssl (GH-134724) (GH-137107)
* [3.14] gh-134698: Hold a lock when the thread state is detached in `ssl` (GH-134724) Lock when the thread state is detached. (cherry picked from commit e047a35) Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Gregory P. Smith <[email protected]> * Only lock the SSL context, not the SSL socket. This solves a deadlock when a socket is blocked while waiting on data, which ended up causing a major regression in 3.13.6 (see gh-137583). --------- Co-authored-by: Peter Bierma <[email protected]>
1 parent ed9c0c3 commit e51acb3

File tree

4 files changed

+123
-48
lines changed

4 files changed

+123
-48
lines changed

Lib/test/test_ssl.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,25 @@ def getpass(self):
12391239
# Make sure the password function isn't called if it isn't needed
12401240
ctx.load_cert_chain(CERTFILE, password=getpass_exception)
12411241

1242+
@threading_helper.requires_working_threading()
1243+
def test_load_cert_chain_thread_safety(self):
1244+
# gh-134698: _ssl detaches the thread state (and as such,
1245+
# releases the GIL and critical sections) around expensive
1246+
# OpenSSL calls. Unfortunately, OpenSSL structures aren't
1247+
# thread-safe, so executing these calls concurrently led
1248+
# to crashes.
1249+
ctx = ssl.create_default_context()
1250+
1251+
def race():
1252+
ctx.load_cert_chain(CERTFILE)
1253+
1254+
threads = [threading.Thread(target=race) for _ in range(8)]
1255+
with threading_helper.catch_threading_exception() as cm:
1256+
with threading_helper.start_threads(threads):
1257+
pass
1258+
1259+
self.assertIsNone(cm.exc_value)
1260+
12421261
def test_load_verify_locations(self):
12431262
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
12441263
ctx.load_verify_locations(CERTFILE)
@@ -4538,6 +4557,42 @@ def server_callback(identity):
45384557
with client_context.wrap_socket(socket.socket()) as s:
45394558
s.connect((HOST, server.port))
45404559

4560+
def test_thread_recv_while_main_thread_sends(self):
4561+
# GH-137583: Locking was added to calls to send() and recv() on SSL
4562+
# socket objects. This seemed fine at the surface level because those
4563+
# calls weren't re-entrant, but recv() calls would implicitly mimick
4564+
# holding a lock by blocking until it received data. This means that
4565+
# if a thread started to infinitely block until data was received, calls
4566+
# to send() would deadlock, because it would wait forever on the lock
4567+
# that the recv() call held.
4568+
data = b"1" * 1024
4569+
event = threading.Event()
4570+
def background(sock):
4571+
event.set()
4572+
received = sock.recv(len(data))
4573+
self.assertEqual(received, data)
4574+
4575+
client_context, server_context, hostname = testing_context()
4576+
server = ThreadedEchoServer(context=server_context)
4577+
with server:
4578+
with client_context.wrap_socket(socket.socket(),
4579+
server_hostname=hostname) as sock:
4580+
sock.connect((HOST, server.port))
4581+
sock.settimeout(1)
4582+
sock.setblocking(1)
4583+
# Ensure that the server is ready to accept requests
4584+
sock.sendall(b"123")
4585+
self.assertEqual(sock.recv(3), b"123")
4586+
with threading_helper.catch_threading_exception() as cm:
4587+
thread = threading.Thread(target=background,
4588+
args=(sock,), daemon=True)
4589+
thread.start()
4590+
event.wait()
4591+
sock.sendall(data)
4592+
thread.join()
4593+
if cm.exc_value is not None:
4594+
raise cm.exc_value
4595+
45414596

45424597
@unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA,
45434598
"Test needs TLS 1.3 PHA")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a crash when calling methods of :class:`ssl.SSLContext` or
2+
:class:`ssl.SSLSocket` across multiple threads.

Modules/_ssl.c

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@
4343
/* Redefined below for Windows debug builds after important #includes */
4444
#define _PySSL_FIX_ERRNO
4545

46-
#define PySSL_BEGIN_ALLOW_THREADS_S(save) \
47-
do { (save) = PyEval_SaveThread(); } while(0)
48-
#define PySSL_END_ALLOW_THREADS_S(save) \
49-
do { PyEval_RestoreThread(save); _PySSL_FIX_ERRNO; } while(0)
50-
#define PySSL_BEGIN_ALLOW_THREADS { \
46+
#define PySSL_BEGIN_ALLOW_THREADS_S(save, mutex) \
47+
do { (save) = PyEval_SaveThread(); PyMutex_Lock(mutex); } while(0)
48+
#define PySSL_END_ALLOW_THREADS_S(save, mutex) \
49+
do { PyMutex_Unlock(mutex); PyEval_RestoreThread(save); _PySSL_FIX_ERRNO; } while(0)
50+
#define PySSL_BEGIN_ALLOW_THREADS(self) { \
5151
PyThreadState *_save = NULL; \
52-
PySSL_BEGIN_ALLOW_THREADS_S(_save);
53-
#define PySSL_END_ALLOW_THREADS PySSL_END_ALLOW_THREADS_S(_save); }
52+
PySSL_BEGIN_ALLOW_THREADS_S(_save, &self->tstate_mutex);
53+
#define PySSL_END_ALLOW_THREADS(self) PySSL_END_ALLOW_THREADS_S(_save, &self->tstate_mutex); }
5454

5555
#if defined(HAVE_POLL_H)
5656
#include <poll.h>
@@ -309,6 +309,9 @@ typedef struct {
309309
PyObject *psk_client_callback;
310310
PyObject *psk_server_callback;
311311
#endif
312+
/* Lock to synchronize calls when the thread state is detached.
313+
See also gh-134698. */
314+
PyMutex tstate_mutex;
312315
} PySSLContext;
313316

314317
#define PySSLContext_CAST(op) ((PySSLContext *)(op))
@@ -889,9 +892,9 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
889892
/* Make sure the SSL error state is initialized */
890893
ERR_clear_error();
891894

892-
PySSL_BEGIN_ALLOW_THREADS
895+
PySSL_BEGIN_ALLOW_THREADS(sslctx)
893896
self->ssl = SSL_new(ctx);
894-
PySSL_END_ALLOW_THREADS
897+
PySSL_END_ALLOW_THREADS(sslctx)
895898
if (self->ssl == NULL) {
896899
Py_DECREF(self);
897900
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
@@ -960,12 +963,12 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
960963
BIO_set_nbio(SSL_get_wbio(self->ssl), 1);
961964
}
962965

963-
PySSL_BEGIN_ALLOW_THREADS
966+
Py_BEGIN_ALLOW_THREADS;
964967
if (socket_type == PY_SSL_CLIENT)
965968
SSL_set_connect_state(self->ssl);
966969
else
967970
SSL_set_accept_state(self->ssl);
968-
PySSL_END_ALLOW_THREADS
971+
Py_END_ALLOW_THREADS;
969972

970973
self->socket_type = socket_type;
971974
if (sock != NULL) {
@@ -1034,10 +1037,11 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self)
10341037
/* Actually negotiate SSL connection */
10351038
/* XXX If SSL_do_handshake() returns 0, it's also a failure. */
10361039
do {
1037-
PySSL_BEGIN_ALLOW_THREADS
1040+
Py_BEGIN_ALLOW_THREADS
10381041
ret = SSL_do_handshake(self->ssl);
10391042
err = _PySSL_errno(ret < 1, self->ssl, ret);
1040-
PySSL_END_ALLOW_THREADS
1043+
Py_END_ALLOW_THREADS;
1044+
_PySSL_FIX_ERRNO;
10411045
self->err = err;
10421046

10431047
if (PyErr_CheckSignals())
@@ -2414,9 +2418,10 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
24142418
ms = (int)_PyTime_AsMilliseconds(timeout, _PyTime_ROUND_CEILING);
24152419
assert(ms <= INT_MAX);
24162420

2417-
PySSL_BEGIN_ALLOW_THREADS
2421+
Py_BEGIN_ALLOW_THREADS
24182422
rc = poll(&pollfd, 1, (int)ms);
2419-
PySSL_END_ALLOW_THREADS
2423+
Py_END_ALLOW_THREADS
2424+
_PySSL_FIX_ERRNO;
24202425
#else
24212426
/* Guard against socket too large for select*/
24222427
if (!_PyIsSelectable_fd(s->sock_fd))
@@ -2428,13 +2433,14 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
24282433
FD_SET(s->sock_fd, &fds);
24292434

24302435
/* Wait until the socket becomes ready */
2431-
PySSL_BEGIN_ALLOW_THREADS
2436+
Py_BEGIN_ALLOW_THREADS
24322437
nfds = Py_SAFE_DOWNCAST(s->sock_fd+1, SOCKET_T, int);
24332438
if (writing)
24342439
rc = select(nfds, NULL, &fds, NULL, &tv);
24352440
else
24362441
rc = select(nfds, &fds, NULL, NULL, &tv);
2437-
PySSL_END_ALLOW_THREADS
2442+
Py_END_ALLOW_THREADS
2443+
_PySSL_FIX_ERRNO;
24382444
#endif
24392445

24402446
/* Return SOCKET_TIMED_OUT on timeout, SOCKET_OPERATION_OK otherwise
@@ -2505,10 +2511,11 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b)
25052511
}
25062512

25072513
do {
2508-
PySSL_BEGIN_ALLOW_THREADS
2514+
Py_BEGIN_ALLOW_THREADS;
25092515
retval = SSL_write_ex(self->ssl, b->buf, (size_t)b->len, &count);
25102516
err = _PySSL_errno(retval == 0, self->ssl, retval);
2511-
PySSL_END_ALLOW_THREADS
2517+
Py_END_ALLOW_THREADS;
2518+
_PySSL_FIX_ERRNO;
25122519
self->err = err;
25132520

25142521
if (PyErr_CheckSignals())
@@ -2566,10 +2573,11 @@ _ssl__SSLSocket_pending_impl(PySSLSocket *self)
25662573
int count = 0;
25672574
_PySSLError err;
25682575

2569-
PySSL_BEGIN_ALLOW_THREADS
2576+
Py_BEGIN_ALLOW_THREADS;
25702577
count = SSL_pending(self->ssl);
25712578
err = _PySSL_errno(count < 0, self->ssl, count);
2572-
PySSL_END_ALLOW_THREADS
2579+
Py_END_ALLOW_THREADS;
2580+
_PySSL_FIX_ERRNO;
25732581
self->err = err;
25742582

25752583
if (count < 0)
@@ -2660,10 +2668,11 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len,
26602668
deadline = _PyDeadline_Init(timeout);
26612669

26622670
do {
2663-
PySSL_BEGIN_ALLOW_THREADS
2671+
Py_BEGIN_ALLOW_THREADS;
26642672
retval = SSL_read_ex(self->ssl, mem, (size_t)len, &count);
26652673
err = _PySSL_errno(retval == 0, self->ssl, retval);
2666-
PySSL_END_ALLOW_THREADS
2674+
Py_END_ALLOW_THREADS;
2675+
_PySSL_FIX_ERRNO;
26672676
self->err = err;
26682677

26692678
if (PyErr_CheckSignals())
@@ -2762,7 +2771,7 @@ _ssl__SSLSocket_shutdown_impl(PySSLSocket *self)
27622771
}
27632772

27642773
while (1) {
2765-
PySSL_BEGIN_ALLOW_THREADS
2774+
Py_BEGIN_ALLOW_THREADS;
27662775
/* Disable read-ahead so that unwrap can work correctly.
27672776
* Otherwise OpenSSL might read in too much data,
27682777
* eating clear text data that happens to be
@@ -2775,7 +2784,8 @@ _ssl__SSLSocket_shutdown_impl(PySSLSocket *self)
27752784
SSL_set_read_ahead(self->ssl, 0);
27762785
ret = SSL_shutdown(self->ssl);
27772786
err = _PySSL_errno(ret < 0, self->ssl, ret);
2778-
PySSL_END_ALLOW_THREADS
2787+
Py_END_ALLOW_THREADS;
2788+
_PySSL_FIX_ERRNO;
27792789
self->err = err;
27802790

27812791
/* If err == 1, a secure shutdown with SSL_shutdown() is complete */
@@ -3167,9 +3177,10 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version)
31673177
// no other thread can be touching this object yet.
31683178
// (Technically, we can't even lock if we wanted to, as the
31693179
// lock hasn't been initialized yet.)
3170-
PySSL_BEGIN_ALLOW_THREADS
3180+
Py_BEGIN_ALLOW_THREADS
31713181
ctx = SSL_CTX_new(method);
3172-
PySSL_END_ALLOW_THREADS
3182+
Py_END_ALLOW_THREADS
3183+
_PySSL_FIX_ERRNO;
31733184

31743185
if (ctx == NULL) {
31753186
_setSSLError(get_ssl_state(module), NULL, 0, __FILE__, __LINE__);
@@ -3194,6 +3205,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version)
31943205
self->psk_client_callback = NULL;
31953206
self->psk_server_callback = NULL;
31963207
#endif
3208+
self->tstate_mutex = (PyMutex){0};
31973209

31983210
/* Don't check host name by default */
31993211
if (proto_version == PY_SSL_VERSION_TLS_CLIENT) {
@@ -3312,9 +3324,10 @@ context_clear(PyObject *op)
33123324
Py_CLEAR(self->psk_server_callback);
33133325
#endif
33143326
if (self->keylog_bio != NULL) {
3315-
PySSL_BEGIN_ALLOW_THREADS
3327+
Py_BEGIN_ALLOW_THREADS
33163328
BIO_free_all(self->keylog_bio);
3317-
PySSL_END_ALLOW_THREADS
3329+
Py_END_ALLOW_THREADS
3330+
_PySSL_FIX_ERRNO;
33183331
self->keylog_bio = NULL;
33193332
}
33203333
return 0;
@@ -4037,7 +4050,8 @@ _password_callback(char *buf, int size, int rwflag, void *userdata)
40374050
_PySSLPasswordInfo *pw_info = (_PySSLPasswordInfo*) userdata;
40384051
PyObject *fn_ret = NULL;
40394052

4040-
PySSL_END_ALLOW_THREADS_S(pw_info->thread_state);
4053+
pw_info->thread_state = PyThreadState_Swap(pw_info->thread_state);
4054+
_PySSL_FIX_ERRNO;
40414055

40424056
if (pw_info->error) {
40434057
/* already failed previously. OpenSSL 3.0.0-alpha14 invokes the
@@ -4067,13 +4081,13 @@ _password_callback(char *buf, int size, int rwflag, void *userdata)
40674081
goto error;
40684082
}
40694083

4070-
PySSL_BEGIN_ALLOW_THREADS_S(pw_info->thread_state);
4084+
pw_info->thread_state = PyThreadState_Swap(pw_info->thread_state);
40714085
memcpy(buf, pw_info->password, pw_info->size);
40724086
return pw_info->size;
40734087

40744088
error:
40754089
Py_XDECREF(fn_ret);
4076-
PySSL_BEGIN_ALLOW_THREADS_S(pw_info->thread_state);
4090+
pw_info->thread_state = PyThreadState_Swap(pw_info->thread_state);
40774091
pw_info->error = 1;
40784092
return -1;
40794093
}
@@ -4126,10 +4140,10 @@ _ssl__SSLContext_load_cert_chain_impl(PySSLContext *self, PyObject *certfile,
41264140
SSL_CTX_set_default_passwd_cb(self->ctx, _password_callback);
41274141
SSL_CTX_set_default_passwd_cb_userdata(self->ctx, &pw_info);
41284142
}
4129-
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
4143+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41304144
r = SSL_CTX_use_certificate_chain_file(self->ctx,
41314145
PyBytes_AS_STRING(certfile_bytes));
4132-
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
4146+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41334147
if (r != 1) {
41344148
if (pw_info.error) {
41354149
ERR_clear_error();
@@ -4144,11 +4158,11 @@ _ssl__SSLContext_load_cert_chain_impl(PySSLContext *self, PyObject *certfile,
41444158
}
41454159
goto error;
41464160
}
4147-
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
4161+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41484162
r = SSL_CTX_use_PrivateKey_file(self->ctx,
41494163
PyBytes_AS_STRING(keyfile ? keyfile_bytes : certfile_bytes),
41504164
SSL_FILETYPE_PEM);
4151-
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
4165+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41524166
Py_CLEAR(keyfile_bytes);
41534167
Py_CLEAR(certfile_bytes);
41544168
if (r != 1) {
@@ -4165,9 +4179,9 @@ _ssl__SSLContext_load_cert_chain_impl(PySSLContext *self, PyObject *certfile,
41654179
}
41664180
goto error;
41674181
}
4168-
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
4182+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41694183
r = SSL_CTX_check_private_key(self->ctx);
4170-
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
4184+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state, &self->tstate_mutex);
41714185
if (r != 1) {
41724186
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
41734187
goto error;
@@ -4384,9 +4398,9 @@ _ssl__SSLContext_load_verify_locations_impl(PySSLContext *self,
43844398
cafile_buf = PyBytes_AS_STRING(cafile_bytes);
43854399
if (capath)
43864400
capath_buf = PyBytes_AS_STRING(capath_bytes);
4387-
PySSL_BEGIN_ALLOW_THREADS
4401+
PySSL_BEGIN_ALLOW_THREADS(self)
43884402
r = SSL_CTX_load_verify_locations(self->ctx, cafile_buf, capath_buf);
4389-
PySSL_END_ALLOW_THREADS
4403+
PySSL_END_ALLOW_THREADS(self)
43904404
if (r != 1) {
43914405
if (errno != 0) {
43924406
PyErr_SetFromErrno(PyExc_OSError);
@@ -4438,10 +4452,11 @@ _ssl__SSLContext_load_dh_params_impl(PySSLContext *self, PyObject *filepath)
44384452
return NULL;
44394453

44404454
errno = 0;
4441-
PySSL_BEGIN_ALLOW_THREADS
4455+
Py_BEGIN_ALLOW_THREADS
44424456
dh = PEM_read_DHparams(f, NULL, NULL, NULL);
44434457
fclose(f);
4444-
PySSL_END_ALLOW_THREADS
4458+
Py_END_ALLOW_THREADS
4459+
_PySSL_FIX_ERRNO;
44454460
if (dh == NULL) {
44464461
if (errno != 0) {
44474462
PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, filepath);
@@ -4593,6 +4608,7 @@ _ssl__SSLContext_set_default_verify_paths_impl(PySSLContext *self)
45934608
Py_BEGIN_ALLOW_THREADS
45944609
rc = SSL_CTX_set_default_verify_paths(self->ctx);
45954610
Py_END_ALLOW_THREADS
4611+
_PySSL_FIX_ERRNO;
45964612
if (!rc) {
45974613
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
45984614
return NULL;

0 commit comments

Comments
 (0)