-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Race in py_digest_by_name, _hashopenssl.c under free-threading #128657
Comments
It appears to be a (somewhat) harmless simultaneous initialization of digest routine by multiple threads. Worst thing that can happen as far as I can see is extra refcount on routine which might prevent it from being freed which is probably not a big issue since it is the same routine for all uses. In any case the fix is simple, it will shut up TSAN and keep refcounts same as in GIL py: $ git diff main
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
index 082929be3c..d05d7b109b 100644
--- a/Modules/_hashopenssl.c
+++ b/Modules/_hashopenssl.c
@@ -26,6 +26,7 @@
#include "Python.h"
#include "pycore_hashtable.h"
#include "pycore_strhex.h" // _Py_strhex()
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_RELAXED
#include "hashlib.h"
/* EVP is the preferred interface to hashing in OpenSSL */
@@ -379,16 +380,28 @@ py_digest_by_name(PyObject *module, const char *name, enum Py_hash_type py_ht)
case Py_ht_evp:
case Py_ht_mac:
case Py_ht_pbkdf2:
- if (entry->evp == NULL) {
- entry->evp = PY_EVP_MD_fetch(entry->ossl_name, NULL);
+ digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp);
+ if (digest == NULL) {
+ digest = PY_EVP_MD_fetch(entry->ossl_name, NULL);
+ // exchange and free just in case another thread did same thing at same time
+ PY_EVP_MD *other_digest = _Py_atomic_exchange_ptr(&entry->evp, digest);
+ if (other_digest != NULL) {
+ assert(other_digest == digest);
+ PY_EVP_MD_free(other_digest);
+ }
}
- digest = entry->evp;
break;
case Py_ht_evp_nosecurity:
- if (entry->evp_nosecurity == NULL) {
- entry->evp_nosecurity = PY_EVP_MD_fetch(entry->ossl_name, "-fips");
+ digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp_nosecurity);
+ if (digest == NULL) {
+ digest = PY_EVP_MD_fetch(entry->ossl_name, "-fips");
+ // exchange and free just in case another thread did same thing at same time
+ PY_EVP_MD *other_digest = _Py_atomic_exchange_ptr(&entry->evp_nosecurity, digest);
+ if (other_digest != NULL) {
+ assert(other_digest == digest);
+ PY_EVP_MD_free(other_digest);
+ }
}
- digest = entry->evp_nosecurity;
break;
}
if (digest != NULL) { I used this as reference for digest refcount https://docs.openssl.org/3.1/man3/EVP_DigestInit/#synopsis, and here is a simpler reproducer: import hashlib
import threading
if __name__ == "__main__":
num_workers = 40
barrier = threading.Barrier(num_workers)
def closure():
barrier.wait()
hashlib.sha256()
for i in range(num_workers):
threading.Thread(target=closure).start() |
The approach looks reasonable to me. We might need to be careful with |
+ PY_EVP_MD *other_digest = _Py_atomic_exchange_ptr(&entry->evp, digest);
+ if (other_digest != NULL) {
+ assert(other_digest == digest);
+ PY_EVP_MD_free(other_digest);
+ } Is it possible that |
The documentation states that the individual algorithm implementations are refcounted which implies the same object is returned for the same name, which is what I saw in execution and is also implied by the original code setting once and reuse of EDIT: The above does need one little tweak to avoid a very unlikely refcount escape, but basically this. |
Bug report
Bug description:
I built cpython (3.13 branch) with free-threading and TSAN. The following python code from time to time reports TSAN warnings:
TSAN report extract:
Full TSAN report
cc @colesbury
CPython versions tested on:
3.13
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: