From ad2aa7b2bc08164fc5b5eda2ae035b4a3d99d8f2 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Tue, 8 Sep 2026 15:58:44 +0300 Subject: [PATCH 1/4] fix(structseq): enable GC tracking in PyStructSequence_New --- Lib/test/test_structseq.py | 5 +++++ .../2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst | 1 + Objects/structseq.c | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 74506fc54de50e7..0f75cd236d32426 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -365,5 +365,10 @@ def test_replace_gc_tracked(self): self.assertTrue(gc.is_tracked(replaced_struct)) + def test_gc_tracked(self): + # PyStructSequence objects created via C API or Python should be GC-tracked + self.assertTrue(gc.is_tracked(time.gmtime())) + self.assertTrue(gc.is_tracked(os.stat(__file__))) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst new file mode 100644 index 000000000000000..a54dfab6a811f1e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst @@ -0,0 +1 @@ +Fix GC tracking for :class:`!PyStructSequence` instances created via :c:func:`PyStructSequence_New`. Patch by Shamil Abdulaev. diff --git a/Objects/structseq.c b/Objects/structseq.c index 9130fe6a133b1e4..7be74e1c01094d2 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -84,6 +84,7 @@ PyStructSequence_New(PyTypeObject *type) for (i = 0; i < size; i++) obj->ob_item[i] = NULL; + _PyObject_GC_TRACK(obj); return (PyObject*)obj; } @@ -265,7 +266,6 @@ structseq_new_impl(PyTypeObject *type, PyObject *arg, PyObject *dict) } } - _PyObject_GC_TRACK(res); return (PyObject*) res; } @@ -449,7 +449,6 @@ structseq_replace(PyObject *op, PyObject *args, PyObject *kwargs) } } - _PyObject_GC_TRACK(result); return (PyObject *)result; error: From 053f101f2cac80cab12086f23d1fbd20e992dc8f Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Tue, 6 Oct 2026 00:01:19 +0300 Subject: [PATCH 2/4] fix(structseq): track struct_time and asyncgen_hooks in GC --- Doc/c-api/tuple.rst | 4 ++++ Lib/test/test_structseq.py | 6 ++++-- Lib/test/test_sys.py | 1 + .../2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst | 2 +- Modules/timemodule.c | 1 + Objects/structseq.c | 3 ++- Python/sysmodule.c | 1 + 7 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index e8be4762dc33a12..638b81e71aa5871 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -232,6 +232,10 @@ type. Creates an instance of *type*, which must have been created with :c:func:`PyStructSequence_NewType`. + The returned instance is not tracked by the garbage collector. If the + instance can contain objects that participate in reference cycles, call + :c:func:`PyObject_GC_Track` after populating its fields. + Return ``NULL`` with an exception set on failure. diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 0f75cd236d32426..079153680d26978 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -366,9 +366,11 @@ def test_replace_gc_tracked(self): self.assertTrue(gc.is_tracked(replaced_struct)) def test_gc_tracked(self): - # PyStructSequence objects created via C API or Python should be GC-tracked + # Instances created via Python or stdlib callers that may participate + # in cycles are GC-tracked; simple instances like os.stat_result are not. self.assertTrue(gc.is_tracked(time.gmtime())) - self.assertTrue(gc.is_tracked(os.stat(__file__))) + self.assertTrue(gc.is_tracked(time.struct_time((2020, 1, 1, 0, 0, 0, 0, 1, 0)))) + self.assertFalse(gc.is_tracked(os.stat(__file__))) if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 4308de227a46ce4..820f96ae3479d29 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1969,6 +1969,7 @@ def test_asyncgen_hooks(self): sys.set_asyncgen_hooks(firstiter=firstiter) hooks = sys.get_asyncgen_hooks() + self.assertTrue(gc.is_tracked(hooks)) self.assertIs(hooks.firstiter, firstiter) self.assertIs(hooks[0], firstiter) self.assertIs(hooks.finalizer, None) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst index a54dfab6a811f1e..4732f413f78407e 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst @@ -1 +1 @@ -Fix GC tracking for :class:`!PyStructSequence` instances created via :c:func:`PyStructSequence_New`. Patch by Shamil Abdulaev. +Track :func:`time.struct_time` and :func:`sys.get_asyncgen_hooks` objects in the garbage collector to prevent reference cycle leaks. Patch by Shamil Abdulaev. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 0005974b52499ce..af7e1abaacec765 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -497,6 +497,7 @@ tmtotuple(time_module_state *state, struct tm *p #undef SET #undef SET_ITEM + PyObject_GC_Track(v); return v; } diff --git a/Objects/structseq.c b/Objects/structseq.c index 7be74e1c01094d2..9130fe6a133b1e4 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -84,7 +84,6 @@ PyStructSequence_New(PyTypeObject *type) for (i = 0; i < size; i++) obj->ob_item[i] = NULL; - _PyObject_GC_TRACK(obj); return (PyObject*)obj; } @@ -266,6 +265,7 @@ structseq_new_impl(PyTypeObject *type, PyObject *arg, PyObject *dict) } } + _PyObject_GC_TRACK(res); return (PyObject*) res; } @@ -449,6 +449,7 @@ structseq_replace(PyObject *op, PyObject *args, PyObject *kwargs) } } + _PyObject_GC_TRACK(result); return (PyObject *)result; error: diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 10087c1ccaac17d..4c688c1b243edf7 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1531,6 +1531,7 @@ sys_get_asyncgen_hooks_impl(PyObject *module) PyStructSequence_SET_ITEM(res, 0, Py_NewRef(firstiter)); PyStructSequence_SET_ITEM(res, 1, Py_NewRef(finalizer)); + PyObject_GC_Track(res); return res; } From a5a15b2755e08369b4e4a5b40330e63e2f561b91 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 7 Oct 2026 00:20:37 +0300 Subject: [PATCH 3/4] fix(time): make struct_time type immutable --- Doc/c-api/tuple.rst | 4 ---- Lib/test/test_structseq.py | 16 +++++++--------- Lib/test/test_sys.py | 1 - ...026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst | 2 +- Modules/timemodule.c | 5 +++-- Python/sysmodule.c | 1 - 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index 638b81e71aa5871..e8be4762dc33a12 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -232,10 +232,6 @@ type. Creates an instance of *type*, which must have been created with :c:func:`PyStructSequence_NewType`. - The returned instance is not tracked by the garbage collector. If the - instance can contain objects that participate in reference cycles, call - :c:func:`PyObject_GC_Track` after populating its fields. - Return ``NULL`` with an exception set on failure. diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 079153680d26978..c4c830c7374953a 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -351,9 +351,9 @@ def test_reference_cycle(self): # was cleared first, the structseq instance would crash in the # destructor. script_helper.assert_python_ok("-c", textwrap.dedent(r""" - import time - t = time.gmtime() - type(t).refcyle = t + import os + t = os.stat('.') + type(t).refcycle = t """)) def test_replace_gc_tracked(self): @@ -365,12 +365,10 @@ def test_replace_gc_tracked(self): self.assertTrue(gc.is_tracked(replaced_struct)) - def test_gc_tracked(self): - # Instances created via Python or stdlib callers that may participate - # in cycles are GC-tracked; simple instances like os.stat_result are not. - self.assertTrue(gc.is_tracked(time.gmtime())) - self.assertTrue(gc.is_tracked(time.struct_time((2020, 1, 1, 0, 0, 0, 0, 1, 0)))) - self.assertFalse(gc.is_tracked(os.stat(__file__))) + def test_struct_time_type_immutable(self): + t = time.gmtime() + with self.assertRaisesRegex(TypeError, "immutable type"): + type(t).refcycle = t if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 820f96ae3479d29..4308de227a46ce4 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1969,7 +1969,6 @@ def test_asyncgen_hooks(self): sys.set_asyncgen_hooks(firstiter=firstiter) hooks = sys.get_asyncgen_hooks() - self.assertTrue(gc.is_tracked(hooks)) self.assertIs(hooks.firstiter, firstiter) self.assertIs(hooks[0], firstiter) self.assertIs(hooks.finalizer, None) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst index 4732f413f78407e..ac2bcf09a1f2fd4 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-08-15-55-00.gh-issue-157176.yZ8a1B.rst @@ -1 +1 @@ -Track :func:`time.struct_time` and :func:`sys.get_asyncgen_hooks` objects in the garbage collector to prevent reference cycle leaks. Patch by Shamil Abdulaev. +Make the :class:`time.struct_time` type immutable to prevent reference cycles between the type and its instances. Patch by Shamil Abdulaev. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index af7e1abaacec765..3f88c8e9dc28960 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -5,6 +5,7 @@ #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_namespace.h" // _PyNamespace_New() #include "pycore_runtime.h" // _Py_ID() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_time.h" // _PyTimeFraction #include // clock() @@ -497,7 +498,6 @@ tmtotuple(time_module_state *state, struct tm *p #undef SET #undef SET_ITEM - PyObject_GC_Track(v); return v; } @@ -2111,7 +2111,8 @@ time_exec(PyObject *module) } // struct_time type - state->struct_time_type = PyStructSequence_NewType(&struct_time_type_desc); + state->struct_time_type = _PyStructSequence_NewType( + &struct_time_type_desc, Py_TPFLAGS_IMMUTABLETYPE); if (state->struct_time_type == NULL) { return -1; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 4c688c1b243edf7..10087c1ccaac17d 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1531,7 +1531,6 @@ sys_get_asyncgen_hooks_impl(PyObject *module) PyStructSequence_SET_ITEM(res, 0, Py_NewRef(firstiter)); PyStructSequence_SET_ITEM(res, 1, Py_NewRef(finalizer)); - PyObject_GC_Track(res); return res; } From e996c400a00c20b2440c8b5ae8f374a0dc9eaf4a Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 7 Oct 2026 10:57:32 +0300 Subject: [PATCH 4/4] test(structseq): use immutable type check --- Lib/test/test_structseq.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index c4c830c7374953a..04b6497265809fc 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -6,6 +6,7 @@ import textwrap import time import unittest +from test import support from test.support import script_helper @@ -366,9 +367,7 @@ def test_replace_gc_tracked(self): self.assertTrue(gc.is_tracked(replaced_struct)) def test_struct_time_type_immutable(self): - t = time.gmtime() - with self.assertRaisesRegex(TypeError, "immutable type"): - type(t).refcycle = t + support.check_immutable_type(self, type(time.gmtime())) if __name__ == "__main__": unittest.main()