Repository navigation
Data races in OpenSSL bindings #143756
Copy link
Copy link
Open
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-SSLtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
We have some data races in our OpenSSL bindings that we were not previously detecting because we didn't compile OpenSSL with TSan (#143750):
test_load_cert_chain_thread_safety
Race between use_certificate_chain_file and SSL_CTX_set_default_passwd_cb:
WARNING: ThreadSanitizer: data race (pid=3543367)
Read of size 8 at 0x726c000007b8 by thread T6:
#0 use_certificate_chain_file /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_rsa.c:477:32 (libssl.so.3+0x80b0f) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#1 SSL_CTX_use_certificate_chain_file /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_rsa.c:587:12 (libssl.so.3+0x80a19) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#2 _ssl__SSLContext_load_cert_chain_impl /home/sgross/cpython/./Modules/_ssl.c:4595:9 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x1f9b8) (BuildId: b7a1f8db907210c3a84626c1dff3c67d6f138a0a)
#3 _ssl__SSLContext_load_cert_chain /home/sgross/cpython/./Modules/clinic/_ssl.c.h:1833:20 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x1f9b8)
...
Previous write of size 8 at 0x726c000007b8 by thread T4:
#0 SSL_CTX_set_default_passwd_cb /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:4497:34 (libssl.so.3+0x6c654) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#1 _ssl__SSLContext_load_cert_chain_impl /home/sgross/cpython/./Modules/_ssl.c:4640:5 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x1fd64) (BuildId: b7a1f8db907210c3a84626c1dff3c67d6f138a0a)
#2 _ssl__SSLContext_load_cert_chain /home/sgross/cpython/./Modules/clinic/_ssl.c.h:1833:20 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x1fd64)
test_thread_recv_while_main_thread_sends
WARNING: ThreadSanitizer: data race (pid=3543528)
Write of size 4 at 0x72880000f068 by main thread:
#0 ssl3_write_bytes /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/record/rec_layer_s3.c:285:16 (libssl.so.3+0x14050b) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#1 ssl3_write /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/s3_lib.c:4648:12 (libssl.so.3+0x3eee9) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#2 ssl_write_internal /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:2604:16 (libssl.so.3+0x65007) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#3 SSL_write_ex2 /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:2707:15 (libssl.so.3+0x653b5) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#4 SSL_write_ex /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:2701:12 (libssl.so.3+0x65339) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#5 _ssl__SSLSocket_write_impl /home/sgross/cpython/./Modules/_ssl.c:2792:18 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x2a809) (BuildId: b7a1f8db907210c3a84626c1dff3c67d6f138a0a)
#6 _ssl__SSLSocket_write /home/sgross/cpython/./Modules/clinic/_ssl.c.h:651:20 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x2a809)
#7 _PyEval_EvalFrameDefault /home/sgross/cpython/Python/generated_cases.c.h:4009:35 (python+0x5129d9) (BuildId: 09bdcea4d998e789f62d66f1eea397f4c0970fe2)
...
Previous write of size 4 at 0x72880000f068 by thread T492:
#0 ssl3_read_bytes /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/record/rec_layer_s3.c:681:16 (libssl.so.3+0x141f69) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#1 ssl3_read_internal /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/s3_lib.c:4666:9 (libssl.so.3+0x3f2ca) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#2 ssl3_read /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/s3_lib.c:4689:12 (libssl.so.3+0x3f127) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#3 ssl_read_internal /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:2379:16 (libssl.so.3+0x63af1) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#4 SSL_read_ex /home/sgross/cpython/multissl/src/openssl-3.5.4/ssl/ssl_lib.c:2407:15 (libssl.so.3+0x64225) (BuildId: 3439d4624f0bbda0d0e957787b11e780635293c6)
#5 _ssl__SSLSocket_read_impl /home/sgross/cpython/./Modules/_ssl.c:2958:18 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x2b229) (BuildId: b7a1f8db907210c3a84626c1dff3c67d6f138a0a)
#6 _ssl__SSLSocket_read /home/sgross/cpython/./Modules/clinic/_ssl.c.h:723:20 (_ssl.cpython-315td-x86_64-linux-gnu.so+0x2b229)
#7 method_vectorcall_VARARGS /home/sgross/cpython/Objects/descrobject.c:325:24 (python+0x293161) (BuildId: 09bdcea4d998e789f62d66f1eea397f4c0970fe2)
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-SSLtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
ZeroIntensity commentedon Jan 12, 2026
load_cert_chainlooks fixable. We should acquiretstate_mutex(viaPySSL_BEGIN_ALLOW_THREADS) around the SSL calls.For
test_thread_recv_while_main_thread_sends, you might be interested in the history here -- specifically #137583. In short, I tried to fix these races a while ago by adding locking around OpenSSL calls, but that ended up deadlocking libraries that attempted to write to a socket while another thread was reading from it. I can't think of any way to fix that without running into that problem again.colesbury commentedon Jan 13, 2026
Thanks for the pointers. I think that locking around the read and write calls is probably the right way to go. python-websockets will need to change their test code, but I think their code is currently broken. That's not the kind of change we should do in a bugfix release, but I think it's appropriate in a major release. cc @gpshead for his thoughts.
Separately, I don't think merging
Py_BEGIN_ALLOW_THREADSwith the mutex acquisition in SSL code makes sense. The mutex should be held across all of the OpenSSL calls in an operation, whilePy_BEGIN/END_ALLOW_THREADSis only for the blocking calls.ZeroIntensity commentedon Jan 13, 2026
I believe @aaugustin expressed the concern that it would be very difficult to implement websockets without the concurrent read/write problem.
Yeah, you're right. I think my original impression was that OpenSSL calls are only done under
Py_BEGIN_ALLOW_THREADSblocks, but that doesn't look correct in hindsight.pythongh-143756: Avoid borrowed reference in SSL code
pythongh-143756: Fix potential data race in SSLContext.load_cert_chain
pythongh-143756: Fix potential data race in SSLContext.load_cert_chain
colesbury commentedon Jan 13, 2026
I wonder if we can configure the OpenSSL BIO's to always be nonblocking and rely on
PySSL_selectfor blocking. That way we could hold the mutex across theSSL_read_exandSSL_write_excalls, but not thePySSL_selectcalls.aaugustin commentedon Jan 16, 2026
It is not a test problem, it's a production problem.
Currently, it's possible to have a thread send a message (
websocket.send(msg)) while another thread blocked waiting for the next message (msg = websocket.recv()). This translates directly to the same operations on a blocking socket — blocking on reads. I expect any other full-duplex protocol implemented with blocking reads to have the same problem.What would be the code change if that becomes impossible?
For a single connection, I guess it would just be a matter of replacing a blocking
recvby a non-blockingselectfollowed by a non-blockingrecv?For a server managing several connections, I suppose the best way is to
select(or equivalent) all connections rather than once per connection, but that's a more significant change that doesn't fit well into websockets' architecture.colesbury commentedon Jan 16, 2026
Thanks for the clarification.
You shouldn't need to change any code. My current plan is for CPython to basically do the thing you described. We'll internally configure the socket to be non-blocking so that the OpenSSL
SSL_read_ex/SSL_write_excalls are non-blocking. The SSLSocketrecv()/send()calls will still block (depending onsettimeout()andsetblocking()) usingselect/poll.We only need the lock around the
SSL_read_ex/SSL_write_excalls, notselect/poll.I don't think it'll be a big change -- this is already how
settimeout()is implemented -- although I'm a little worried about implementation details leaking through something likesocket.fileno().aaugustin commentedon Jan 18, 2026
That would be amazing 🤩
gh-143756: Fix potential data race in SSLContext.load_cert_chain (gh-…
gh-143756: Avoid borrowed reference in SSL code (gh-143816)
pythongh-143756: Fix potential data race in SSLContext.load_cert_chain (
pythongh-143756: Avoid borrowed reference in SSL code (pythongh-143816)
divVerent commentedon Jun 16, 2026
Just as an example, a repro to get a crash out of the
SSL_read/SSL_writedata race is here, tsan or asan not needed:crashing with heap corruption:
As such, this probably should be fixed - I guess the idea above to basically do the
SSL_read/SSL_writecalls under the mutex, but nonblocking, instead waiting prior to them withselect, should work - with the one caveat thatselectindicating there are bytes may not imply theSSL_readwill actually read something, but it may just update internal state (so if the contract to the caller is that at least 1 byte must be read, it will have to loop).Be aware that the mutex probably should be held up until the
SSL_get_errorcall to ensure other threads can't interfere with the error value returned; so basically keep it with the `_PySSL_errno as a block, like it looks now.pythongh-143756: skip test_ssl_in_multiple_threads under TSAN
pythongh-143756: skip test_load_cert_chain_thread_safety under TSAN
devdanzin commentedon Aug 4, 2026
One thing worth putting on the record before a fix is written, because it rules out an approach that looks obvious:
@critical_sectionprovides no mutual exclusion across a GIL-released region.Python/pystate.c:2323, in the detach path:So any critical section held on entry is suspended for the duration of a
Py_BEGIN_ALLOW_THREADS/PySSL_BEGIN_ALLOW_THREADSblock, and re-acquired on reattach. For_sslthat means the 88@critical_sectionclinic directives in the module do not protect theSSL_*call they wrap — only the Python-level bookkeeping on either side of it.Demonstrated without a sanitizer: two threads driven through a barrier end up simultaneously inside one
SSL*, with OpenSSL reporting its owninternal error.This matches the direction the thread is already going — the mutex has to be held across the OpenSSL calls, and
Py_BEGIN/END_ALLOW_THREADSis only for the blocking ones — but it is worth being explicit that a fix expressed as "add@critical_sectionto these methods" would compile, look right, and change nothing.Same point applies to gh-150191.
Written with AI assistance; the
pystate.ccoordinate was read at4f3be1b5777and the two-thread demonstration was run.ci: 抑制 OpenSSL PBKDF2 的已知 TSan 竞争