From 09080326b0819eb7705144ade9a81ebd89361b14 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 13 Aug 2026 05:15:02 -0700 Subject: [PATCH] gh-155515: Use GC tracking for HAMT iterators (GH-155517) (cherry picked from commit f0562124927ec0087a5d3460efe08567aab64b84) Co-authored-by: Neil Schemenauer --- Lib/test/test_context.py | 25 +++++++++++++++++++ ...-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst | 4 +++ Python/hamt.c | 9 +++++++ 3 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index ef20495dcc01ea9..f193a3335210591 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 000000000000000..96b9c608780c2ec --- /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 95998ae5062ac7e..92ce686863964a5 100644 --- a/Python/hamt.c +++ b/Python/hamt.c @@ -2451,6 +2451,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; } @@ -2497,6 +2501,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; } @@ -2517,6 +2525,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; }