https://github.com/python/cpython/commit/33a65116dfff106038bca1c57ae3116b0a4dad06
commit: 33a65116dfff106038bca1c57ae3116b0a4dad06
branch: 3.13
author: Bénédikt Tran <[email protected]>
committer: Yhg1s <[email protected]>
date: 2026-09-29T21:57:07Z
summary:

[3.13] gh-155835: fix `digest_size` and `block_size` data races on SHA-3 
objects (GH-155838) (#157041)

* [3.13] gh-155835: fix `digest_size` and `block_size` data races on SHA-3 
objects (GH-155838)
(cherry picked from commit 90ac5398e49a80d3c2976621f5a8e449ec0db7d5)

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-15-13-12-09.gh-issue-155835.DsbSmy.rst
M Lib/test/test_hashlib.py
M Modules/sha3module.c

diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py
index 424ebe1ed5af2f..1bd7fb3b24a4f6 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,16 +54,42 @@
     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)
+SKIP_SHA3 = _sha3 is None or support.check_sanitizer(ub=True)
 requires_sha3 = unittest.skipUnless(not SKIP_SHA3, 'requires _sha3')
 
 
@@ -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 *

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to