Skip to content

Commit 57f1bad

Browse files
[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 08500b0) Co-authored-by: Jason G <41053218+gianghungtien@users.noreply.github.com>
1 parent ba0e4a4 commit 57f1bad

3 files changed

Lines changed: 24 additions & 0 deletions

File tree

‎Lib/test/test_tuple.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ def check_one_exact(t, e32, e64):
119119
check_one_exact((0.5, (), (-2, 3, (4, 6))), 714642271,
120120
-1845940830829704396)
121121

122+
@support.skip_if_huge_c_stack(500_000)
123+
@support.skip_wasi_stack_overflow()
124+
@support.skip_emscripten_stack_overflow()
125+
def test_hash_deeply_nested(self):
126+
# This should raise a RecursionError and not crash.
127+
# See https://github.com/python/cpython/issues/154318.
128+
t = ()
129+
for _ in range(500_000):
130+
t = (t,)
131+
with self.assertRaisesRegex(RecursionError, "while hashing a tuple"):
132+
hash(t)
133+
122134
# Various tests for hashing of tuples to check that we get few collisions.
123135
# Does something only if RUN_ALL_HASH_TESTS is true.
124136
#
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a crash when hashing a deeply nested :class:`tuple`.
2+
A :exc:`RecursionError` is now raised instead of overflowing the C stack.

‎Objects/tupleobject.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,26 @@ tuple_hash(PyObject *op)
331331

332332
Py_ssize_t len = Py_SIZE(v);
333333
PyObject **item = v->ob_item;
334+
335+
/* Hashing an item recurses back into tuple_hash() when that item is
336+
itself a tuple, so guard the descent to raise RecursionError rather
337+
than overflow the C stack on a deeply nested tuple. */
338+
if (_Py_EnterRecursiveCall(" while hashing a tuple")) {
339+
return -1;
340+
}
341+
334342
acc = _PyTuple_HASH_XXPRIME_5;
335343
for (Py_ssize_t i = 0; i < len; i++) {
336344
Py_uhash_t lane = PyObject_Hash(item[i]);
337345
if (lane == (Py_uhash_t)-1) {
346+
_Py_LeaveRecursiveCall();
338347
return -1;
339348
}
340349
acc += lane * _PyTuple_HASH_XXPRIME_2;
341350
acc = _PyTuple_HASH_XXROTATE(acc);
342351
acc *= _PyTuple_HASH_XXPRIME_1;
343352
}
353+
_Py_LeaveRecursiveCall();
344354

345355
/* Add input length, mangled to keep the historical value of hash(()). */
346356
acc += len ^ (_PyTuple_HASH_XXPRIME_5 ^ 3527539UL);

0 commit comments

Comments
 (0)