diff --git a/Misc/NEWS.d/next/Library/2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst b/Misc/NEWS.d/next/Library/2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst new file mode 100644 index 00000000000000..eb273865894aae --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst @@ -0,0 +1,3 @@ +:mod:`hashlib`: fix a crash arising from a race condition when looking +up an OpenSSL hash algorithm by name the first time. Patch by Bénédikt Tran. + diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index f895c9037485c4..bd1c5f1ad1cbc6 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -692,43 +692,46 @@ static PY_EVP_MD * get_openssl_evp_md_by_utf8name(_hashlibstate *state, const char *name, Py_hash_type py_ht) { - PY_EVP_MD *digest = NULL, *other_digest = NULL; + PY_EVP_MD *digest = NULL, *fresh = NULL; py_hashentry_t *entry = _Py_hashtable_get(state->hashtable, name); + int fips = !disable_fips_property(py_ht); if (entry != NULL) { - if (!disable_fips_property(py_ht)) { - digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp); - if (digest == NULL) { - digest = PY_EVP_MD_fetch(entry->ossl_name, NULL); -#ifdef Py_GIL_DISABLED - // exchange just in case another thread did same thing at same time - other_digest = _Py_atomic_exchange_ptr(&entry->evp, (void *)digest); -#else - entry->evp = digest; -#endif - } - } - else { - digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp_nosecurity); - if (digest == NULL) { - digest = PY_EVP_MD_fetch(entry->ossl_name, "-fips"); -#ifdef Py_GIL_DISABLED - // exchange just in case another thread did same thing at same time - other_digest = _Py_atomic_exchange_ptr(&entry->evp_nosecurity, (void *)digest); -#else - entry->evp_nosecurity = digest; -#endif + PY_EVP_MD **cached = fips ? &entry->evp : &entry->evp_nosecurity; + digest = FT_ATOMIC_LOAD_PTR_RELAXED(*cached); + if (digest == NULL) { + fresh = PY_EVP_MD_fetch(entry->ossl_name, fips ? NULL : "-fips"); + if (fresh != NULL) { + /* + * Publish a freshly fetched digest into the cache exactly once. + * + * On the first publication, 'cached' adopts the 'fresh' ref. + * If another thread wins the race, 'fresh' is released and + * the cached digest is used instead. + * + * See https://github.com/python/cpython/issues/128657 + * and https://github.com/python/cpython/issues/149816. + */ + PY_EVP_MD *expected = NULL; + if (_Py_atomic_compare_exchange_ptr(cached, &expected, (void *)fresh)) { + digest = fresh; // we won: 'cached' now owns the fresh digest + } + else { + PY_EVP_MD_free(fresh); // we lost: discard the fresh digest + digest = expected; // reuse the winner's digest + } } } - // if another thread same thing at same time make sure we got same ptr - assert(other_digest == NULL || other_digest == digest); - if (digest != NULL && other_digest == NULL) { + /* Hand the caller its own reference; the cache keeps its own. Valid + * whether `digest` was already cached, freshly published by this + * thread, or published by a thread that won the race. */ + if (digest != NULL) { PY_EVP_MD_up_ref(digest); } } else { // Fall back for looking up an unindexed OpenSSL specific name. - const char *props = disable_fips_property(py_ht) ? "-fips" : NULL; + const char *props = fips ? NULL : "-fips"; (void)props; // will only be used in OpenSSL 3.0 and later digest = PY_EVP_MD_fetch(name, props); }