diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index ef20495dcc01ea..f193a333521059 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -1282,6 +1282,31 @@ def test_hamt_gc_2(self): self.assertIsNone(ref()) + def test_hamt_gc_3(self): + # gh-154535: the iterators must be tracked by the GC, otherwise a + # cycle running through one is never collected and the HAMT it + # holds -- and everything in it -- leaks. + A = HashKey(100, 'A') + + container = [] + h = hamt() + h = h.set(A, container) + + hi = h.items() + self.assertTrue(gc.is_tracked(hi)) + + # Close the cycle: hi -> h -> container -> hi. + container.append(hi) + ref = weakref.ref(h) + + del h, hi, container + + gc.collect() + gc.collect() + gc.collect() + + self.assertIsNone(ref()) + def test_hamt_in_1(self): A = HashKey(100, 'A') AA = HashKey(100, 'A') diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst new file mode 100644 index 00000000000000..96b9c608780c2e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst @@ -0,0 +1,4 @@ +Track the internal HAMT iterators, which back iteration over a +:class:`contextvars.Context`, with the garbage collector. A reference cycle +running through such an iterator was never collected, leaking the whole +context it iterated over. diff --git a/Python/hamt.c b/Python/hamt.c index 98ef96df2c54e3..f138a948df7dd6 100644 --- a/Python/hamt.c +++ b/Python/hamt.c @@ -2450,6 +2450,10 @@ hamt_baseiter_tp_clear(PyObject *op) { PyHamtIterator *it = (PyHamtIterator*)op; Py_CLEAR(it->hi_obj); + /* i_nodes holds borrowed pointers into the tree that hi_obj was keeping + alive, so the cursor must not be used again. A negative i_level makes + hamt_iterator_next() report I_END without touching i_nodes. */ + it->hi_iter.i_level = -1; return 0; } @@ -2496,6 +2500,10 @@ static Py_ssize_t hamt_baseiter_tp_len(PyObject *op) { PyHamtIterator *it = (PyHamtIterator*)op; + if (it->hi_obj == NULL) { + /* tp_clear() ran on this iterator. */ + return 0; + } return it->hi_obj->h_count; } @@ -2516,6 +2524,7 @@ hamt_baseiter_new(PyTypeObject *type, binaryfunc yield, PyHamtObject *o) hamt_iterator_init(&it->hi_iter, o->h_root); + PyObject_GC_Track(it); return (PyObject*)it; }