From 5e0a10695c3801fd1df5c1544c72e94a8f624fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:27:20 -0700 Subject: [PATCH 1/3] gh-159815: do not leave stale OpenSSL errors in `_hashlib` `raise_unsupported_algorithm_impl()` peeked the OpenSSL error queue without clearing it on the `ERR_R_UNSUPPORTED` and `ERR_R_FETCH_FAILED` branches, so a later unrelated failure in the same thread reported the earlier error. Clear the queue in those branches, and clear it in callers before the OpenSSL operations whose failure is reported by that function or before calling `get_asn1_utf8name_by_nid()`, which inspects the last error. --- Modules/_hashopenssl.c | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index d23767afeb96cfb..7b410ddb6e8e74f 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -508,6 +508,10 @@ notify_smart_ssl_error_occurred_in(const char *funcname) } #ifdef Py_HAS_OPENSSL3_SUPPORT +/* + * The OpenSSL error queue must be cleared before calling the OpenSSL + * function whose failure is being reported. + */ static void raise_unsupported_algorithm_impl(PyObject *exc_type, const char *fallback_format, @@ -518,19 +522,21 @@ raise_unsupported_algorithm_impl(PyObject *exc_type, int errcode = ERR_peek_last_error(); switch (ERR_GET_REASON(errcode)) { case ERR_R_UNSUPPORTED: { + const char *lib = ERR_lib_error_string(errcode); + ERR_clear_error(); PyObject *text = PyUnicode_FromFormat(fallback_format, format_arg); if (text != NULL) { - const char *lib = ERR_lib_error_string(errcode); set_exception_with_ssl_errinfo(exc_type, text, lib, NULL); Py_DECREF(text); } break; } case ERR_R_FETCH_FAILED: { + const char *lib = ERR_lib_error_string(errcode); + const char *reason = ERR_reason_error_string(errcode); + ERR_clear_error(); PyObject *text = PyUnicode_FromFormat(fallback_format, format_arg); if (text != NULL) { - const char *lib = ERR_lib_error_string(errcode); - const char *reason = ERR_reason_error_string(errcode); set_exception_with_ssl_errinfo(exc_type, text, lib, reason); Py_DECREF(text); } @@ -601,6 +607,8 @@ get_hashentry_by_nid(int nid) * Convert the NID to a string via OBJ_nid2*() functions. * * If 'nid' cannot be resolved, set an exception and return NULL. + * + * The OpenSSL error queue must be cleared before calling this function. */ static const char * get_asn1_utf8name_by_nid(int nid) @@ -639,7 +647,11 @@ static const char * get_hashlib_utf8name_by_nid(int nid) { const py_hashentry_t *e = get_hashentry_by_nid(nid); - return e ? e->py_name : get_asn1_utf8name_by_nid(nid); + if (e != NULL) { + return e->py_name; + } + ERR_clear_error(); + return get_asn1_utf8name_by_nid(nid); } #ifdef Py_HAS_OPENSSL3_SUPPORT @@ -652,7 +664,11 @@ static const char * get_openssl_utf8name_by_nid(int nid) { const py_hashentry_t *e = get_hashentry_by_nid(nid); - return e ? e->ossl_name : get_asn1_utf8name_by_nid(nid); + if (e != NULL) { + return e->ossl_name; + } + ERR_clear_error(); + return get_asn1_utf8name_by_nid(nid); } #endif @@ -700,6 +716,7 @@ get_openssl_evp_md_by_utf8name(_hashlibstate *state, const char *name, if (!disable_fips_property(py_ht)) { digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp); if (digest == NULL) { + ERR_clear_error(); 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 @@ -712,6 +729,7 @@ get_openssl_evp_md_by_utf8name(_hashlibstate *state, const char *name, else { digest = FT_ATOMIC_LOAD_PTR_RELAXED(entry->evp_nosecurity); if (digest == NULL) { + ERR_clear_error(); 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 @@ -731,6 +749,7 @@ get_openssl_evp_md_by_utf8name(_hashlibstate *state, const char *name, // Fall back for looking up an unindexed OpenSSL specific name. const char *props = disable_fips_property(py_ht) ? "-fips" : NULL; (void)props; // will only be used in OpenSSL 3.0 and later + ERR_clear_error(); digest = PY_EVP_MD_fetch(name, props); } if (digest == NULL) { @@ -765,6 +784,7 @@ get_openssl_evp_md(_hashlibstate *state, PyObject *digestmod, Py_hash_type py_ht } if (name == NULL) { if (!PyErr_Occurred()) { + ERR_clear_error(); raise_unsupported_algorithm_error(state, digestmod); } return NULL; @@ -1889,6 +1909,7 @@ _hashlib_hmac_singleshot_impl(PyObject *module, Py_buffer *key, assert(evp != NULL); is_xof = PY_EVP_MD_xof(evp); + ERR_clear_error(); Py_BEGIN_ALLOW_THREADS result = EVP_Q_mac( NULL, OSSL_MAC_NAME_HMAC, NULL, NULL, @@ -1907,6 +1928,7 @@ _hashlib_hmac_singleshot_impl(PyObject *module, Py_buffer *key, } is_xof = PY_EVP_MD_xof(evp); + ERR_clear_error(); Py_BEGIN_ALLOW_THREADS result = HMAC( evp, @@ -2082,6 +2104,7 @@ hashlib_HMAC_CTX_new_from_digestmod(_hashlibstate *state, return NULL; } + ERR_clear_error(); r = EVP_MAC_init( ctx, (const unsigned char *)key->buf, @@ -2102,6 +2125,7 @@ hashlib_HMAC_CTX_new_from_digestmod(_hashlibstate *state, return NULL; } + ERR_clear_error(); r = HMAC_Init_ex(ctx, key->buf, (int)key->len, md, NULL /* impl */); PY_EVP_MD_free(md); #endif From 16cd71634faeb9528361d49da0d576344b88ed6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:58:56 -0700 Subject: [PATCH 2/3] add NEWS entry --- .../next/Library/2026-10-19-16-58-56.gh-issue-159815.HfOxkG.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-19-16-58-56.gh-issue-159815.HfOxkG.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-19-16-58-56.gh-issue-159815.HfOxkG.rst b/Misc/NEWS.d/next/Library/2026-10-19-16-58-56.gh-issue-159815.HfOxkG.rst new file mode 100644 index 000000000000000..4ce81aa7ca27300 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-19-16-58-56.gh-issue-159815.HfOxkG.rst @@ -0,0 +1,2 @@ +:mod:`hashlib`: fix exception messages that reported an earlier and unrelated +OpenSSL error after a hash algorithm could not be found. Patch by Bénédikt Tran. From 0a4837efce355c6b3a0578fcbb492c019a792795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:04:53 -0700 Subject: [PATCH 3/3] add test --- Lib/test/test_hashlib.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index a718be415b99055..d1ebe1baa3d83bd 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -1440,6 +1440,18 @@ def scrypt(password=b"password", /, **kwargs): MAX_DKLEN = ((1 << 32) - 1) * 32 # see RFC 7914 self.assertRaises(numeric_exc_types, scrypt, dklen=MAX_DKLEN + 1) + def test_scrypt_stale_openssl_error(self): + # gh-159815: a failed algorithm lookup must not leave an error in + # the OpenSSL error queue that a later unrelated failure reports. + def get_error_message(): + with self.assertRaises(ValueError) as cm: + hashlib.scrypt(b'password', salt=b'salt', n=2, r=0, p=1) + return str(cm.exception) + + expect = get_error_message() + self.assertRaises(ValueError, _hashlib.new, 'no_such_algorithm') + self.assertEqual(get_error_message(), expect) + @threading_helper.requires_working_threading() class TestTSAN(unittest.TestCase):