From ea5a4de3ee5c1fa26aa4b367c83ac108c54974cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:55:40 -0700 Subject: [PATCH 1/4] [3.14] gh-149816: fix a race when fetching OpenSSL digests (GH-153019) (cherry picked from commit 23469818538cf39108535b8f182f3527639d519b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- ...-07-04-12-52-22.gh-issue-149816.XQOutB.rst | 3 + Modules/_hashopenssl.c | 97 ++++++++++--------- 2 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst 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 000000000000000..eb273865894aae0 --- /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 f3b2f10ffb2e851..26595b41c52ca53 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -392,65 +392,68 @@ py_digest_name(const EVP_MD *md) return PyUnicode_FromString(name); } +static inline int +disable_fips_property(enum Py_hash_type py_ht) +{ + switch (py_ht) { + case Py_ht_evp: + case Py_ht_mac: + case Py_ht_pbkdf2: + return 0; + case Py_ht_evp_nosecurity: + return 1; + default: + Py_FatalError("unsupported hash type"); + } +} + /* Get EVP_MD by HID and purpose */ static PY_EVP_MD* py_digest_by_name(PyObject *module, const char *name, enum Py_hash_type py_ht) { - PY_EVP_MD *digest = NULL; - PY_EVP_MD *other_digest = NULL; + PY_EVP_MD *digest = NULL, *fresh = NULL; _hashlibstate *state = get_hashlib_state(module); - py_hashentry_t *entry = (py_hashentry_t *)_Py_hashtable_get( - state->hashtable, (const void*)name - ); + py_hashentry_t *entry = _Py_hashtable_get(state->hashtable, name); + int fips = disable_fips_property(py_ht); if (entry != NULL) { - switch (py_ht) { - case Py_ht_evp: - case Py_ht_mac: - case Py_ht_pbkdf2: - 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 + 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 + } } - break; - case Py_ht_evp_nosecurity: - 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 - } - break; } - // if another thread same thing at same time make sure we got same ptr - assert(other_digest == NULL || other_digest == digest); + /* 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) { - if (other_digest == NULL) { - PY_EVP_MD_up_ref(digest); - } + PY_EVP_MD_up_ref(digest); } - } else { + } + else { // Fall back for looking up an unindexed OpenSSL specific name. - switch (py_ht) { - case Py_ht_evp: - case Py_ht_mac: - case Py_ht_pbkdf2: - digest = PY_EVP_MD_fetch(name, NULL); - break; - case Py_ht_evp_nosecurity: - digest = PY_EVP_MD_fetch(name, "-fips"); - break; - } + 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); } if (digest == NULL) { raise_ssl_error(state->unsupported_digestmod_error, From 65556d74adb9479addefa2628e5b6d2cc3fb7a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:15:31 -0700 Subject: [PATCH 2/4] Update 2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst --- .../next/Library/2026-07-04-12-52-22.gh-issue-149816.XQOutB.rst | 1 - 1 file changed, 1 deletion(-) 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 index eb273865894aae0..b4686eb4b0cb620 100644 --- 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 @@ -1,3 +1,2 @@ :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. - From 7adeff06ccd1512e417667ded16dc2c4b9872553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:30:17 -0700 Subject: [PATCH 3/4] Update _hashopenssl.c --- Modules/_hashopenssl.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index 26595b41c52ca53..b6340a1494842e8 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -204,7 +204,7 @@ py_hashentry_t_destroy_value(void *entry) { } if (h->evp_nosecurity != NULL) { PY_EVP_MD_free(h->evp_nosecurity); - h->evp_nosecurity = NULL; + h->evp_nosecurity = NULL;x } PyMem_Free(entry); } @@ -403,7 +403,10 @@ disable_fips_property(enum Py_hash_type py_ht) case Py_ht_evp_nosecurity: return 1; default: - Py_FatalError("unsupported hash type"); + // To preserve the legacy behavior, we do not raise a fatal error here + // but delegate it to a regular ValueError. This should however never + // happen in practice since Py_hash_type is internal. + return -1; } } @@ -415,6 +418,9 @@ py_digest_by_name(PyObject *module, const char *name, enum Py_hash_type py_ht) _hashlibstate *state = get_hashlib_state(module); py_hashentry_t *entry = _Py_hashtable_get(state->hashtable, name); int fips = disable_fips_property(py_ht); + if (fips == -1) { + goto finally; + } if (entry != NULL) { PY_EVP_MD **cached = fips ? &entry->evp : &entry->evp_nosecurity; @@ -455,6 +461,8 @@ py_digest_by_name(PyObject *module, const char *name, enum Py_hash_type py_ht) (void)props; // will only be used in OpenSSL 3.0 and later digest = PY_EVP_MD_fetch(name, props); } + +finally: if (digest == NULL) { raise_ssl_error(state->unsupported_digestmod_error, "unsupported hash type %s", name); From ceba61dd868cc6811180e255e53870c373c64faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:30:52 -0700 Subject: [PATCH 4/4] Update Modules/_hashopenssl.c --- Modules/_hashopenssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index b6340a1494842e8..09aa2af173e7e5b 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -204,7 +204,7 @@ py_hashentry_t_destroy_value(void *entry) { } if (h->evp_nosecurity != NULL) { PY_EVP_MD_free(h->evp_nosecurity); - h->evp_nosecurity = NULL;x + h->evp_nosecurity = NULL; } PyMem_Free(entry); }