From 20d278623d1e63d3167ba49fbd8099bdece45c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:30:57 +0200 Subject: [PATCH 1/3] gh-155843: properly initialize HMAC objects to prevent crashes after allocation failures (#155845) --- ...-08-15-13-54-22.gh-issue-155843.PD5Af3.rst | 2 + Modules/hmacmodule.c | 41 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-13-54-22.gh-issue-155843.PD5Af3.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-15-13-54-22.gh-issue-155843.PD5Af3.rst b/Misc/NEWS.d/next/Library/2026-08-15-13-54-22.gh-issue-155843.PD5Af3.rst new file mode 100644 index 000000000000000..dd743a71369141e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-13-54-22.gh-issue-155843.PD5Af3.rst @@ -0,0 +1,2 @@ +:mod:`hmac`: ensure that HMAC objects are properly initialized to prevent +rare crashes on allocation failures. Patch by Bénédikt Tran. diff --git a/Modules/hmacmodule.c b/Modules/hmacmodule.c index 0f9eca2f73bd0c5..96a91ce9754bdc0 100644 --- a/Modules/hmacmodule.c +++ b/Modules/hmacmodule.c @@ -271,14 +271,6 @@ get_hmacmodule_state(PyObject *module) return (hmacmodule_state *)state; } -static inline hmacmodule_state * -get_hmacmodule_state_by_cls(PyTypeObject *cls) -{ - void *state = PyType_GetModuleState(cls); - assert(state != NULL); - return (hmacmodule_state *)state; -} - // --- HMAC Object ------------------------------------------------------------ typedef Hacl_Streaming_HMAC_agile_state HACL_HMAC_state; @@ -676,6 +668,24 @@ has_uint32_t_buffer_length(const Py_buffer *buffer) // --- HMAC object ------------------------------------------------------------ +/* + * Create a zero-initialized untracked HMAC object. + * + * Return NULL on failure with an exception set. + */ +static HMACObject * +hmac_new_object(PyTypeObject *tp) +{ + HMACObject *self = (HMACObject *)tp->tp_alloc(tp, 0); + if (self == NULL) { + return NULL; + } + HASHLIB_INIT_MUTEX(self); + // tp_alloc initializes the memory to zero but the unknown kind is -1 + self->kind = Py_hmac_kind_hash_unknown; + return self; +} + /* * Use the HMAC information 'info' to populate the corresponding fields. * @@ -687,7 +697,7 @@ hmac_set_hinfo(hmacmodule_state *state, HMACObject *self, const py_hmac_hinfo *info) { assert(info->display_name != NULL); - self->name = Py_NewRef(info->display_name); + Py_XSETREF(self->name, Py_NewRef(info->display_name)); assert_is_static_hmac_hash_kind(info->kind); self->kind = narrow_hmac_hash_kind(state, info->kind); assert(info->block_size <= Py_hmac_hash_max_block_size); @@ -756,16 +766,15 @@ _hmac_new_impl(PyObject *module, PyObject *keyobj, PyObject *msgobj, return NULL; } - HMACObject *self = PyObject_New(HMACObject, state->hmac_type); + HMACObject *self = hmac_new_object(state->hmac_type); if (self == NULL) { return NULL; } - HASHLIB_INIT_MUTEX(self); hmac_set_hinfo(state, self, info); int rc; // Create the HACL* internal state with the given key. Py_buffer key; - GET_BUFFER_VIEW_OR_ERROR(keyobj, &key, goto error_on_key); + GET_BUFFER_VIEW_OR_ERROR(keyobj, &key, goto error); rc = hmac_new_initial_state(self, key.buf, key.len); PyBuffer_Release(&key); if (rc < 0) { @@ -793,8 +802,6 @@ _hmac_new_impl(PyObject *module, PyObject *keyobj, PyObject *msgobj, assert(rc == 0); return (PyObject *)self; -error_on_key: - self->state = NULL; error: Py_DECREF(self); return NULL; @@ -807,7 +814,7 @@ static void hmac_copy_hinfo(HMACObject *out, const HMACObject *src) { assert(src->name != NULL); - out->name = Py_NewRef(src->name); + Py_XSETREF(out->name, Py_NewRef(src->name)); assert(src->kind != Py_hmac_kind_hash_unknown); out->kind = src->kind; assert(src->block_size <= Py_hmac_hash_max_block_size); @@ -850,8 +857,7 @@ static PyObject * _hmac_HMAC_copy_impl(HMACObject *self, PyTypeObject *cls) /*[clinic end generated code: output=a955bfa55b65b215 input=17b2c0ad0b147e36]*/ { - hmacmodule_state *state = get_hmacmodule_state_by_cls(cls); - HMACObject *copy = PyObject_New(HMACObject, state->hmac_type); + HMACObject *copy = hmac_new_object(cls); if (copy == NULL) { return NULL; } @@ -868,7 +874,6 @@ _hmac_HMAC_copy_impl(HMACObject *self, PyTypeObject *cls) return NULL; } - HASHLIB_INIT_MUTEX(copy); return (PyObject *)copy; } From 90ac5398e49a80d3c2976621f5a8e449ec0db7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:47:11 +0200 Subject: [PATCH 2/3] gh-155835: fix `digest_size` and `block_size` data races on SHA-3 objects (#155838) --- Lib/test/test_hashlib.py | 92 ++++++++++++++++++- ...-08-15-13-12-09.gh-issue-155835.DsbSmy.rst | 3 + Modules/sha3module.c | 34 ++++--- 3 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-13-12-09.gh-issue-155835.DsbSmy.rst diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index 253a8f455853f58..b511c160c9260ff 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -18,6 +18,8 @@ import tempfile import threading import unittest +from functools import partial +from operator import attrgetter from test import support from test.support import _4G, bigmemtest from test.support import hashlib_helper @@ -52,18 +54,39 @@ def get_fips_mode(): return 0 + +try: + import _md5 +except ImportError: + _md5 = None +requires_md5 = unittest.skipUnless(_md5, 'requires _md5') + + try: import _blake2 except ImportError: _blake2 = None - requires_blake2 = unittest.skipUnless(_blake2, 'requires _blake2') + +try: + import _sha1 +except ImportError: + _sha1 = None +requires_sha1 = unittest.skipUnless(_sha1, 'requires _sha1') + + +try: + import _sha2 +except ImportError: + _sha2 = None +requires_sha2 = unittest.skipUnless(_sha2, 'requires _sha2') + + try: import _sha3 except ImportError: _sha3 = None - requires_sha3 = unittest.skipUnless(_sha3, 'requires _sha3') @@ -1418,5 +1441,70 @@ def scrypt(password=b"password", /, **kwargs): self.assertRaises(numeric_exc_types, scrypt, dklen=MAX_DKLEN + 1) +@threading_helper.requires_working_threading() +class TestTSAN(unittest.TestCase): + + @threading_helper.reap_threads + def check_attribute(self, write, read, expected, nthreads=8): + ready = threading.Event() + barrier = threading.Barrier(nthreads) + + def writer(): + barrier.wait() + while not ready.is_set(): + write() + + def reader(): + barrier.wait() + while not ready.is_set(): + self.assertEqual(read(), expected) + + targets = [writer if i % 2 else reader for i in range(nthreads)] + workers = [threading.Thread(target=target) for target in targets] + with threading_helper.start_threads(workers, unlock=ready.set): + pass + + def check_HACL_attribute(self, module, version, attrname): + blob = b"A" * 65536 + obj = getattr(module, version)() + update = partial(obj.update, blob) + read = attrgetter(attrname) + self.check_attribute(update, partial(read, obj), read(obj)) + + @requires_md5 + @support.subTests("attrname", ["block_size", "digest_size"]) + def test_HACL_md5_attributes(self, attrname): + self.check_HACL_attribute(_md5, "md5", attrname) + + @requires_sha1 + @support.subTests("attrname", ["block_size", "digest_size"]) + def test_HACL_sha1_attributes(self, attrname): + self.check_HACL_attribute(_sha1, "sha1", attrname) + + @requires_sha2 + @support.subTests("size", [224, 256, 384, 512]) + @support.subTests("attrname", ["block_size", "digest_size"]) + def test_HACL_sha2_attributes(self, size, attrname): + self.check_HACL_attribute(_sha2, f"sha{size}", attrname) + + @requires_sha3 + @support.subTests("size", [224, 256, 384, 512]) + @support.subTests( + "attrname", + ["block_size", "digest_size", "_capacity_bits", "_rate_bits"], + ) + def test_HACL_sha3_attributes(self, size, attrname): + self.check_HACL_attribute(_sha3, f"sha3_{size}", attrname) + + @requires_sha3 + @support.subTests("size", [128, 256]) + @support.subTests( + "attrname", + ["block_size", "digest_size", "_capacity_bits", "_rate_bits"], + ) + def test_HACL_shake_attributes(self, size, attrname): + self.check_HACL_attribute(_sha3, f"shake_{size}", attrname) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-08-15-13-12-09.gh-issue-155835.DsbSmy.rst b/Misc/NEWS.d/next/Library/2026-08-15-13-12-09.gh-issue-155835.DsbSmy.rst new file mode 100644 index 000000000000000..7c48daad1c29d8f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-13-12-09.gh-issue-155835.DsbSmy.rst @@ -0,0 +1,3 @@ +:mod:`hashlib`: Fix data races when accessing +:attr:`~hashlib.hash.digest_size` and :attr:`~hashlib.hash.block_size` on +SHA-3 objects. Patch by Bénédikt Tran. diff --git a/Modules/sha3module.c b/Modules/sha3module.c index 3ddd0323575b708..3f694238de86541 100644 --- a/Modules/sha3module.c +++ b/Modules/sha3module.c @@ -67,6 +67,12 @@ sha3_get_state(PyObject *module) typedef struct { HASHLIB_OBJECT_HEAD Hacl_Hash_SHA3_state_t *hash_state; + // HACL* update functions entirely replace the state, which can lead + // to races on the free-threaded build. Since the kind of hash is static, + // we can store its corresponding metadata once. + uint32_t digest_size; + uint32_t block_size; + int is_shake; } SHA3object; #define _SHA3object_CAST(op) ((SHA3object *)(op)) @@ -96,7 +102,7 @@ newSHA3object(PyTypeObject *type) return NULL; } HASHLIB_INIT_MUTEX(newobj); - + newobj->digest_size = newobj->block_size = 0; PyObject_GC_Track(newobj); return newobj; } @@ -179,6 +185,11 @@ py_sha3_new_impl(PyTypeObject *type, PyObject *data_obj, int usedforsecurity, goto error; } + // set the metadata once we know that the state is valid + int is_shake = Hacl_Hash_SHA3_is_shake(self->hash_state); + self->digest_size = is_shake ? 0 : Hacl_Hash_SHA3_hash_len(self->hash_state); + self->block_size = Hacl_Hash_SHA3_block_len(self->hash_state); + if (data) { GET_BUFFER_VIEW_OR_ERROR(data, &buf, goto error); /* Do not use self->mutex here as this is the constructor @@ -253,6 +264,8 @@ _sha3_sha3_224_copy_impl(SHA3object *self, PyTypeObject *cls) Py_DECREF(newobj); return PyErr_NoMemory(); } + newobj->digest_size = self->digest_size; + newobj->block_size = self->block_size; return (PyObject *)newobj; } @@ -273,8 +286,7 @@ _sha3_sha3_224_digest_impl(SHA3object *self) HASHLIB_ACQUIRE_LOCK(self); (void)Hacl_Hash_SHA3_digest(self->hash_state, digest); HASHLIB_RELEASE_LOCK(self); - return PyBytes_FromStringAndSize((const char *)digest, - Hacl_Hash_SHA3_hash_len(self->hash_state)); + return PyBytes_FromStringAndSize((const char *)digest, self->digest_size); } @@ -292,8 +304,7 @@ _sha3_sha3_224_hexdigest_impl(SHA3object *self) HASHLIB_ACQUIRE_LOCK(self); (void)Hacl_Hash_SHA3_digest(self->hash_state, digest); HASHLIB_RELEASE_LOCK(self); - return _Py_strhex((const char *)digest, - Hacl_Hash_SHA3_hash_len(self->hash_state)); + return _Py_strhex((const char *)digest, self->digest_size); } @@ -334,8 +345,7 @@ static PyObject * SHA3_get_block_size(PyObject *op, void *Py_UNUSED(closure)) { SHA3object *self = _SHA3object_CAST(op); - uint32_t rate = Hacl_Hash_SHA3_block_len(self->hash_state); - return PyLong_FromLong(rate); + return PyLong_FromLong(self->block_size); } @@ -371,10 +381,7 @@ SHA3_get_digest_size(PyObject *op, void *Py_UNUSED(closure)) { // Preserving previous behavior: variable-length algorithms return 0 SHA3object *self = _SHA3object_CAST(op); - if (Hacl_Hash_SHA3_is_shake(self->hash_state)) - return PyLong_FromLong(0); - else - return PyLong_FromLong(Hacl_Hash_SHA3_hash_len(self->hash_state)); + return PyLong_FromLong(self->digest_size); } @@ -382,7 +389,7 @@ static PyObject * SHA3_get_capacity_bits(PyObject *op, void *Py_UNUSED(closure)) { SHA3object *self = _SHA3object_CAST(op); - uint32_t rate = Hacl_Hash_SHA3_block_len(self->hash_state) * 8; + uint32_t rate = self->block_size * 8; assert(rate <= 1600); int capacity = 1600 - rate; return PyLong_FromLong(capacity); @@ -393,8 +400,7 @@ static PyObject * SHA3_get_rate_bits(PyObject *op, void *Py_UNUSED(closure)) { SHA3object *self = _SHA3object_CAST(op); - uint32_t rate = Hacl_Hash_SHA3_block_len(self->hash_state) * 8; - return PyLong_FromLong(rate); + return PyLong_FromLong(self->block_size * 8); } static PyObject * From 878b5e256aa40c45e12a0cd5c63f8a249f7c3704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:49:09 +0200 Subject: [PATCH 3/3] gh-155835: fix `digest_size` data race on BLAKE-2 objects (#155839) --- Lib/test/test_hashlib.py | 6 ++++++ .../Library/2026-08-15-13-18-01.gh-issue-155835.mlyLWp.rst | 2 ++ Modules/blake2module.c | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-13-18-01.gh-issue-155835.mlyLWp.rst diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index b511c160c9260ff..a718be415b99055 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -1505,6 +1505,12 @@ def test_HACL_sha3_attributes(self, size, attrname): def test_HACL_shake_attributes(self, size, attrname): self.check_HACL_attribute(_sha3, f"shake_{size}", attrname) + @requires_blake2 + @support.subTests("version", ["blake2s", "blake2b"]) + @support.subTests("attrname", ["block_size", "digest_size"]) + def test_HACL_blake2_attributes(self, version, attrname): + self.check_HACL_attribute(_blake2, version, attrname) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-08-15-13-18-01.gh-issue-155835.mlyLWp.rst b/Misc/NEWS.d/next/Library/2026-08-15-13-18-01.gh-issue-155835.mlyLWp.rst new file mode 100644 index 000000000000000..9ef7d0694e66c31 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-13-18-01.gh-issue-155835.mlyLWp.rst @@ -0,0 +1,2 @@ +:mod:`hashlib`: Fix a data race when accessing :attr:`~hashlib.hash.digest_size` +on BLAKE-2 objects. Patch by Bénédikt Tran. diff --git a/Modules/blake2module.c b/Modules/blake2module.c index ac7265bb9d6836c..c69c4259b12666f 100644 --- a/Modules/blake2module.c +++ b/Modules/blake2module.c @@ -950,7 +950,9 @@ static PyObject * py_blake2b_get_digest_size(PyObject *op, void *Py_UNUSED(closure)) { Blake2Object *self = _Blake2Object_CAST(op); + HASHLIB_ACQUIRE_LOCK(self); Hacl_Hash_Blake2b_index info = hacl_get_blake2_info(self); + HASHLIB_RELEASE_LOCK(self); return PyLong_FromLong(info.digest_length); }