https://github.com/python/cpython/commit/1fdd1dc568a1084978e17dbbc92302da7aed3bf9
commit: 1fdd1dc568a1084978e17dbbc92302da7aed3bf9
branch: 3.14
author: Bénédikt Tran <[email protected]>
committer: encukou <[email protected]>
date: 2026-10-12T13:35:30+02:00
summary:
[3.14] gh-155835: fix `digest_size` and `block_size` data races on SHA-3
objects (GH-155838) (GH-157039)
(cherry picked from commit 90ac5398e49a80d3c2976621f5a8e449ec0db7d5)
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 fa13b553c88f86..01f2c1ed79511a 100644
--- a/Lib/test/test_hashlib.py
+++ b/Lib/test/test_hashlib.py
@@ -19,6 +19,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 hashlib_helper
@@ -53,18 +55,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')
@@ -1301,5 +1324,141 @@ def readable(self):
hashlib.file_digest(NonBlocking(), hashlib.sha256)
[email protected](hasattr(hashlib, 'scrypt'), 'requires OpenSSL 1.1+')
[email protected](get_fips_mode(), reason="scrypt is blocked in FIPS mode")
+class TestScrypt(unittest.TestCase):
+
+ scrypt_test_vectors = [
+ (b'', b'', 16, 1, 1,
unhexlify('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')),
+ (b'password', b'NaCl', 1024, 8, 16,
unhexlify('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')),
+ (b'pleaseletmein', b'SodiumChloride', 16384, 8, 1,
unhexlify('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')),
+ ]
+
+ def test_scrypt(self):
+ for password, salt, n, r, p, expected in self.scrypt_test_vectors:
+ result = hashlib.scrypt(password, salt=salt, n=n, r=r, p=p)
+ self.assertEqual(result, expected)
+
+ # these parameters must be valid
+ hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1)
+ hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1, maxmem=0)
+ hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1, dklen=1)
+
+ def test_scrypt_types(self):
+ # password and salt must be bytes-like
+ with self.assertRaises(TypeError):
+ hashlib.scrypt('password', salt=b'salt', n=2, r=8, p=1)
+ with self.assertRaises(TypeError):
+ hashlib.scrypt(b'password', salt='salt', n=2, r=8, p=1)
+ # require keyword args
+ with self.assertRaises(TypeError):
+ hashlib.scrypt(b'password')
+ with self.assertRaises(TypeError):
+ hashlib.scrypt(b'password', b'salt')
+ with self.assertRaises(TypeError):
+ hashlib.scrypt(b'password', 2, 8, 1, salt=b'salt')
+
+ def test_scrypt_validate(self):
+ def scrypt(password=b"password", /, **kwargs):
+ # overwrite well-defined parameters with bad ones
+ kwargs = dict(salt=b'salt', n=2, r=8, p=1) | kwargs
+ return hashlib.scrypt(password, **kwargs)
+
+ for param_name in ('n', 'r', 'p', 'maxmem', 'dklen'):
+ param = {param_name: None}
+ with self.subTest(**param):
+ self.assertRaises(TypeError, scrypt, **param)
+
+ self.assertRaises(ValueError, scrypt, n=0)
+ self.assertRaises(ValueError, scrypt, n=-1)
+ self.assertRaises(ValueError, scrypt, n=1)
+
+ self.assertRaises(ValueError, scrypt, r=0)
+ self.assertRaises(ValueError, scrypt, r=-1)
+
+ self.assertRaises(ValueError, scrypt, p=-1)
+ self.assertRaises(ValueError, scrypt, p=0)
+
+ self.assertRaises(ValueError, scrypt, maxmem=-1)
+ # OpenSSL hard limit for 'maxmem' is an 'uint64_t' but for now,
+ # we do not use the 'uint64' Clinic converter but the 'long' one.
+ self.assertRaises(OverflowError, scrypt, maxmem=(1 << 64))
+ # Historically, Python allowed 'maxmem' to be at most INT_MAX,
+ # which is at most 2**32-1 (on Windows, sizeof(long) == 4, so
+ # an OverflowError will be raised instead of a ValueError).
+ numeric_exc_types = (OverflowError, ValueError)
+ self.assertRaises(numeric_exc_types, scrypt, maxmem=(1 << 32))
+
+ self.assertRaises(ValueError, scrypt, dklen=-1)
+ self.assertRaises(ValueError, scrypt, dklen=0)
+ MAX_DKLEN = ((1 << 32) - 1) * 32 # see RFC 7914
+ 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 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 cfbf0cbcc042c5..1c48a83c99db2c 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;
#define _SHA3object_CAST(op) ((SHA3object *)(op))
@@ -78,7 +84,7 @@ newSHA3object(PyTypeObject *type)
return NULL;
}
HASHLIB_INIT_MUTEX(newobj);
-
+ newobj->digest_size = newobj->block_size = 0;
PyObject_GC_Track(newobj);
return newobj;
}
@@ -161,6 +167,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);
if (buf.len >= HASHLIB_GIL_MINSIZE) {
@@ -245,6 +256,8 @@ _sha3_sha3_224_copy_impl(SHA3object *self)
Py_DECREF(newobj);
return PyErr_NoMemory();
}
+ newobj->digest_size = self->digest_size;
+ newobj->block_size = self->block_size;
return (PyObject *)newobj;
}
@@ -265,8 +278,7 @@ _sha3_sha3_224_digest_impl(SHA3object *self)
ENTER_HASHLIB(self);
(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);
}
@@ -284,8 +296,7 @@ _sha3_sha3_224_hexdigest_impl(SHA3object *self)
ENTER_HASHLIB(self);
(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);
}
@@ -337,8 +348,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);
}
@@ -374,10 +384,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);
}
@@ -385,7 +392,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);
@@ -396,8 +403,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 *
_______________________________________________
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]