Skip to content

Commit b3e7902

Browse files
[3.15] gh-154318: Guard C recursion when hashing nested tuples and frozendicts (GH-154362) (#160128)
gh-154318: Guard C recursion when hashing nested tuples and frozendicts (GH-154362) * 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 0cd10d0 commit b3e7902

5 files changed

Lines changed: 49 additions & 0 deletions

File tree

‎Lib/test/test_dict.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,6 +1973,21 @@ def test_hash(self):
19731973
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
19741974
hash(fd)
19751975

1976+
@support.skip_if_huge_c_stack(500_000)
1977+
@support.skip_wasi_stack_overflow()
1978+
@support.skip_emscripten_stack_overflow()
1979+
def test_hash_deeply_nested(self):
1980+
# This should raise a RecursionError and not crash.
1981+
# Nesting through *values*: unlike keys, values are not hashed at
1982+
# construction time, so hashing recurses the full depth.
1983+
# See https://github.com/python/cpython/issues/154318.
1984+
fd = frozendict({0: 0})
1985+
for _ in range(500_000):
1986+
fd = frozendict({0: fd})
1987+
with self.assertRaisesRegex(RecursionError,
1988+
"while hashing a frozendict"):
1989+
hash(fd)
1990+
19761991
@support.cpython_only
19771992
def test_hash_cpython(self):
19781993
# Check that hash(frozendict) implementation is:

‎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` or :class:`frozendict`.
2+
A :exc:`RecursionError` is now raised instead of overflowing the C stack.

‎Objects/dictobject.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8409,16 +8409,26 @@ frozendict_hash(PyObject *op)
84098409
PyDictObject *mp = _PyAnyDict_CAST(op);
84108410
Py_uhash_t hash = 0;
84118411

8412+
/* Hashing a value recurses back into frozendict_hash() when that value
8413+
is itself a frozendict, so guard the descent to raise RecursionError
8414+
rather than overflow the C stack on a deeply nested frozendict. Keys
8415+
are safe: their hashes are already computed at construction time. */
8416+
if (_Py_EnterRecursiveCall(" while hashing a frozendict")) {
8417+
return -1;
8418+
}
8419+
84128420
PyObject *value; // borrowed ref
84138421
Py_ssize_t pos = 0;
84148422
Py_hash_t key_hash;
84158423
while (_PyDict_Next(op, &pos, NULL, &value, &key_hash)) {
84168424
Py_hash_t pair_hash = frozendict_pair_hash(key_hash, value);
84178425
if (pair_hash == -1) {
8426+
_Py_LeaveRecursiveCall();
84188427
return -1;
84198428
}
84208429
hash ^= _shuffle_bits(pair_hash);
84218430
}
8431+
_Py_LeaveRecursiveCall();
84228432

84238433
/* Factor in the number of active entries */
84248434
hash ^= ((Py_uhash_t)mp->ma_used + 1) * 1927868237UL;

‎Objects/tupleobject.c‎

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

381381
Py_ssize_t len = Py_SIZE(v);
382382
PyObject **item = v->ob_item;
383+
384+
/* Hashing an item recurses back into tuple_hash() when that item is
385+
itself a tuple, so guard the descent to raise RecursionError rather
386+
than overflow the C stack on a deeply nested tuple. */
387+
if (_Py_EnterRecursiveCall(" while hashing a tuple")) {
388+
return -1;
389+
}
390+
383391
acc = _PyTuple_HASH_XXPRIME_5;
384392
for (Py_ssize_t i = 0; i < len; i++) {
385393
Py_uhash_t lane = PyObject_Hash(item[i]);
386394
if (lane == (Py_uhash_t)-1) {
395+
_Py_LeaveRecursiveCall();
387396
return -1;
388397
}
389398
acc += lane * _PyTuple_HASH_XXPRIME_2;
390399
acc = _PyTuple_HASH_XXROTATE(acc);
391400
acc *= _PyTuple_HASH_XXPRIME_1;
392401
}
402+
_Py_LeaveRecursiveCall();
393403

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

0 commit comments

Comments
 (0)