From b3965e583b03437dcec147a8d0711ccf35d385c1 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 1/2] [3.13] gh-155835: fix `digest_size` and `block_size` data races on SHA-3 objects (GH-155838) (cherry picked from commit 90ac5398e49a80d3c2976621f5a8e449ec0db7d5) 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> --- Lib/test/test_hashlib.py | 99 ++++++++++++++++++- ...-08-15-13-12-09.gh-issue-155835.DsbSmy.rst | 3 + Modules/sha3module.c | 48 ++++++--- 3 files changed, 131 insertions(+), 19 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 424ebe1ed5af2f..3f9c790b477bde 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -18,6 +18,8 @@ import threading import unittest import warnings +from functools import partial +from operator import attrgetter from test import support from test.support import _4G, bigmemtest from test.support.import_helper import import_fresh_module @@ -52,17 +54,43 @@ 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 # bpo-46913: Don't test the _sha3 extension on a Python UBSAN build # TODO(gh-99108): Revisit this after _sha3 uses HACL*. -SKIP_SHA3 = support.check_sanitizer(ub=True) -requires_sha3 = unittest.skipUnless(not SKIP_SHA3, 'requires _sha3') +SKIP_SHA3 = _sha3 is None or support.check_sanitizer(ub=True) +requires_sha3 = unittest.skipUnkess(not SKIP_SHA3, 'requires _sha3') def hexstr(s): @@ -1273,5 +1301,70 @@ def readable(self): hashlib.file_digest(NonBlocking(), hashlib.sha256) +@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 00000000000000..7c48daad1c29d8 --- /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 79146c2985ef7f..46befa1b057ed0 100644 --- a/Modules/sha3module.c +++ b/Modules/sha3module.c @@ -64,6 +64,12 @@ typedef struct { bool use_mutex; PyMutex mutex; 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; #include "clinic/sha3module.c.h" @@ -77,7 +83,7 @@ newSHA3object(PyTypeObject *type) return NULL; } HASHLIB_INIT_MUTEX(newobj); - + newobj->digest_size = newobj->block_size = 0; return newobj; } @@ -143,6 +149,16 @@ py_sha3_new_impl(PyTypeObject *type, PyObject *data_obj, int usedforsecurity, goto error; } + if (self->hash_state == NULL) { + (void)PyErr_NoMemory(); + 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); if (buf.len >= HASHLIB_GIL_MINSIZE) { @@ -204,6 +220,12 @@ _sha3_sha3_224_copy_impl(SHA3object *self) ENTER_HASHLIB(self); newobj->hash_state = Hacl_Hash_SHA3_copy(self->hash_state); LEAVE_HASHLIB(self); + if (newobj->hash_state == NULL) { + Py_DECREF(newobj); + return PyErr_NoMemory(); + } + newobj->digest_size = self->digest_size; + newobj->block_size = self->block_size; return (PyObject *)newobj; } @@ -222,10 +244,9 @@ _sha3_sha3_224_digest_impl(SHA3object *self) // This function errors out if the algorithm is Shake. Here, we know this // not to be the case, and therefore do not perform error checking. ENTER_HASHLIB(self); - Hacl_Hash_SHA3_digest(self->hash_state, digest); + (void)Hacl_Hash_SHA3_digest(self->hash_state, digest); LEAVE_HASHLIB(self); - return PyBytes_FromStringAndSize((const char *)digest, - Hacl_Hash_SHA3_hash_len(self->hash_state)); + return PyBytes_FromStringAndSize((const char *)digest, self->digest_size); } @@ -241,10 +262,9 @@ _sha3_sha3_224_hexdigest_impl(SHA3object *self) { unsigned char digest[SHA3_MAX_DIGESTSIZE]; ENTER_HASHLIB(self); - Hacl_Hash_SHA3_digest(self->hash_state, digest); + (void)Hacl_Hash_SHA3_digest(self->hash_state, digest); LEAVE_HASHLIB(self); - return _Py_strhex((const char *)digest, - Hacl_Hash_SHA3_hash_len(self->hash_state)); + return _Py_strhex((const char *)digest, self->digest_size); } @@ -295,8 +315,7 @@ static PyMethodDef SHA3_methods[] = { static PyObject * SHA3_get_block_size(SHA3object *self, void *closure) { - uint32_t rate = Hacl_Hash_SHA3_block_len(self->hash_state); - return PyLong_FromLong(rate); + return PyLong_FromLong(self->block_size); } @@ -331,17 +350,15 @@ static PyObject * SHA3_get_digest_size(SHA3object *self, void *closure) { // Preserving previous behavior: variable-length algorithms return 0 - 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); } static PyObject * SHA3_get_capacity_bits(SHA3object *self, void *closure) { - 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); } @@ -350,8 +367,7 @@ SHA3_get_capacity_bits(SHA3object *self, void *closure) static PyObject * SHA3_get_rate_bits(SHA3object *self, void *closure) { - 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 8b804ff28b8f0ec49848c3892022bc52b5619db4 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 17:02:10 +0200 Subject: [PATCH 2/2] fixup --- Lib/test/test_hashlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index 3f9c790b477bde..1bd7fb3b24a4f6 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -90,7 +90,7 @@ def get_fips_mode(): # bpo-46913: Don't test the _sha3 extension on a Python UBSAN build # TODO(gh-99108): Revisit this after _sha3 uses HACL*. SKIP_SHA3 = _sha3 is None or support.check_sanitizer(ub=True) -requires_sha3 = unittest.skipUnkess(not SKIP_SHA3, 'requires _sha3') +requires_sha3 = unittest.skipUnless(not SKIP_SHA3, 'requires _sha3') def hexstr(s):