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]
