https://github.com/python/cpython/commit/57f1bad491b6228d71dbca05c2b3c99ddf2b67fe
commit: 57f1bad491b6228d71dbca05c2b3c99ddf2b67fe
branch: 3.14
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-21T16:38:53-07:00
summary:

[3.14] gh-154318: Guard C recursion when hashing nested tuples and frozendicts 
(GH-154362) (#160131)

* gh-154318: Guard C recursion when hashing nested tuples and frozendicts

tuple_hash() and frozendict_hash() fold each contained object's hash into
an accumulator with PyObject_Hash() inside an unguarded loop. For a deeply
nested tuple or frozendict this recurses back into the same function once
per level and overflows the C stack (SIGSEGV / STATUS_STACK_OVERFLOW)
instead of raising RecursionError.

Wrap both loops in _Py_EnterRecursiveCall() / _Py_LeaveRecursiveCall(), as
the sibling comparison and repr paths already do. The guard is placed after
the cached-hash early return, so repeated hashing of an already-hashed
tuple is unaffected.

This also covers types.GenericAlias, whose ga_hash() hashes its __args__
tuple and so crashed through tuple_hash().

(cherry picked from commit 08500b05e1526f2d840a716e332aaf4e58c56b6a)

Co-authored-by: Jason G <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-10-42-15.gh-issue-154318.Kq3Wm7.rst
M Lib/test/test_tuple.py
M Objects/tupleobject.c

diff --git a/Lib/test/test_tuple.py b/Lib/test/test_tuple.py
index e533392b8cae94..1b6c6f46d6e7da 100644
--- a/Lib/test/test_tuple.py
+++ b/Lib/test/test_tuple.py
@@ -119,6 +119,18 @@ def check_one_exact(t, e32, e64):
         check_one_exact((0.5, (), (-2, 3, (4, 6))), 714642271,
                         -1845940830829704396)
 
+    @support.skip_if_huge_c_stack(500_000)
+    @support.skip_wasi_stack_overflow()
+    @support.skip_emscripten_stack_overflow()
+    def test_hash_deeply_nested(self):
+        # This should raise a RecursionError and not crash.
+        # See https://github.com/python/cpython/issues/154318.
+        t = ()
+        for _ in range(500_000):
+            t = (t,)
+        with self.assertRaisesRegex(RecursionError, "while hashing a tuple"):
+            hash(t)
+
     # Various tests for hashing of tuples to check that we get few collisions.
     # Does something only if RUN_ALL_HASH_TESTS is true.
     #
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-10-42-15.gh-issue-154318.Kq3Wm7.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-10-42-15.gh-issue-154318.Kq3Wm7.rst
new file mode 100644
index 00000000000000..800435bc627034
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-10-42-15.gh-issue-154318.Kq3Wm7.rst
@@ -0,0 +1,2 @@
+Fix a crash when hashing a deeply nested :class:`tuple`.
+A :exc:`RecursionError` is now raised instead of overflowing the C stack.
diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c
index 6e9b36864a12b4..709a66d607fb3f 100644
--- a/Objects/tupleobject.c
+++ b/Objects/tupleobject.c
@@ -331,16 +331,26 @@ tuple_hash(PyObject *op)
 
     Py_ssize_t len = Py_SIZE(v);
     PyObject **item = v->ob_item;
+
+    /* Hashing an item recurses back into tuple_hash() when that item is
+       itself a tuple, so guard the descent to raise RecursionError rather
+       than overflow the C stack on a deeply nested tuple. */
+    if (_Py_EnterRecursiveCall(" while hashing a tuple")) {
+        return -1;
+    }
+
     acc = _PyTuple_HASH_XXPRIME_5;
     for (Py_ssize_t i = 0; i < len; i++) {
         Py_uhash_t lane = PyObject_Hash(item[i]);
         if (lane == (Py_uhash_t)-1) {
+            _Py_LeaveRecursiveCall();
             return -1;
         }
         acc += lane * _PyTuple_HASH_XXPRIME_2;
         acc = _PyTuple_HASH_XXROTATE(acc);
         acc *= _PyTuple_HASH_XXPRIME_1;
     }
+    _Py_LeaveRecursiveCall();
 
     /* Add input length, mangled to keep the historical value of hash(()). */
     acc += len ^ (_PyTuple_HASH_XXPRIME_5 ^ 3527539UL);

_______________________________________________
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