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

hugovk <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/6d13dc528b6b61edbafd9b9da5a726b8708f325e
commit: 6d13dc528b6b61edbafd9b9da5a726b8708f325e
branch: 3.13
author: Han Lee | 이한결 <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-17T06:48:45+03:00
summary:

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

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 ce313a5bd825b46..d2094224ee17573 100644
--- a/Lib/test/test_context.py
+++ b/Lib/test/test_context.py
@@ -1096,6 +1096,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 98c8cc3192a0e6d..4cbdb498af68fb4 100644
--- a/Python/hamt.c
+++ b/Python/hamt.c
@@ -2487,6 +2487,10 @@ static int
 hamt_baseiter_tp_clear(PyHamtIterator *it)
 {
     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;
 }
 
@@ -2530,6 +2534,10 @@ hamt_baseiter_tp_iternext(PyHamtIterator *it)
 static Py_ssize_t
 hamt_baseiter_tp_len(PyHamtIterator *it)
 {
+    if (it->hi_obj == NULL) {
+        /* tp_clear() ran on this iterator. */
+        return 0;
+    }
     return it->hi_obj->h_count;
 }
 
@@ -2550,6 +2558,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.