[3.14] gh-155515: Use GC tracking for HAMT iterators (GH-155517) (#155698)

hugovk <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/04350d911168c981f1504c3e0213478b21821219
commit: 04350d911168c981f1504c3e0213478b21821219
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-17T06:40:05+03:00
summary:

[3.14] gh-155515: Use GC tracking for HAMT iterators (GH-155517) (#155698)

Co-authored-by: Neil Schemenauer <[email protected]>

files:
A Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst
M Lib/test/test_context.py
M Python/hamt.c

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 98ef96df2c54e37..f138a948df7dd63 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;
 }
 

_______________________________________________
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.