From 7f46f512b89e8a8283b224b5b2da06dfddb1993b Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 26 Aug 2026 19:37:36 +0500 Subject: [PATCH 1/4] Untrack tuples from marshal --- Python/marshal.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/marshal.c b/Python/marshal.c index b11f2dca57a226..9765e0df2d86fb 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -1433,6 +1433,7 @@ r_object(RFILE *p) } PyTuple_SET_ITEM(v, i, v2); } + _PyTuple_MaybeUntrack(v); retval = v; break; From 5a7dbf494dfd25784d11b9690cfab39199df417c Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 26 Aug 2026 19:37:45 +0500 Subject: [PATCH 2/4] Add tests --- Lib/test/test_marshal.py | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 0e71d65f22b4f0..8ce95e7510804a 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -1,3 +1,5 @@ +import gc + from test import support from test.support import is_apple_mobile, os_helper, requires_debug_ranges, is_emscripten from test.support.script_helper import assert_python_ok @@ -872,6 +874,55 @@ def test_read_object_from_file(self): _testcapi.pymarshal_read_object_from_file(os_helper.TESTFN) os_helper.unlink(os_helper.TESTFN) +@support.cpython_only +class GCTrackingTestCase(unittest.TestCase): + + def _not_tracked_instantly(self, t): + new = marshal.loads(marshal.dumps(t)) + + self.assertFalse(gc.is_tracked(t), t) + self.assertFalse(gc.is_tracked(new), new) + + def _not_tracked(self, t): + # Nested tuples can take several collections to untrack + gc.collect() + gc.collect() + + new = marshal.loads(marshal.dumps(t)) + + self.assertFalse(gc.is_tracked(t), t) + self.assertFalse(gc.is_tracked(new), new) + + def _tracked(self, t): + new = marshal.loads(marshal.dumps(t)) + + self.assertTrue(gc.is_tracked(t), t) + self.assertTrue(gc.is_tracked(new), new) + + def testTuple(self): + x, y, z = 1.5, "a", [] + + self._not_tracked_instantly(()) + self._not_tracked_instantly((1,)) + self._not_tracked_instantly((1, 2)) + self._not_tracked_instantly((1, 2, "a")) + self._not_tracked_instantly((12, 10**10, 'a_' * 100)) + + # Test for _PyTuple_Concat + self._not_tracked_instantly((1, 2) + (2, 3)) + + # Test for _PyTuple_Repeat + self._not_tracked_instantly((1, 2) * 5) + + self._not_tracked(((1, x), y, (2, 3))) + self._not_tracked((1, 2, (True, False, ()))) + + self._tracked(([],)) + self._tracked(([1],)) + self._tracked(({},)) + self._tracked((set(),)) + self._tracked((x, y, z)) + if __name__ == "__main__": unittest.main() From 98bca4a46bb26ecfa7cde241f08b99cad96292ff Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 26 Aug 2026 19:40:10 +0500 Subject: [PATCH 3/4] Add news --- .../2026-08-26-19-40-02.gh-issue-155728.OvrUCl.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-19-40-02.gh-issue-155728.OvrUCl.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-19-40-02.gh-issue-155728.OvrUCl.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-19-40-02.gh-issue-155728.OvrUCl.rst new file mode 100644 index 00000000000000..7597b58e670ae4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-19-40-02.gh-issue-155728.OvrUCl.rst @@ -0,0 +1,2 @@ +Untrack tuples from the GC when possible during unmarshalling. Patch by +Sergey Miryanov. From 97526e51b15448a507416bfef97e4dc3f0db16b1 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 26 Aug 2026 21:08:47 +0500 Subject: [PATCH 4/4] Address review and fix tests --- Python/marshal.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Python/marshal.c b/Python/marshal.c index 9765e0df2d86fb..c77e2d04b15b7f 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -1180,6 +1180,7 @@ r_object(RFILE *p) int type, code = r_byte(p); int flag, is_interned = 0; PyObject *retval = NULL; + bool track_tuple = false; if (code == EOF) { if (PyErr_ExceptionMatches(PyExc_EOFError)) { @@ -1422,6 +1423,11 @@ r_object(RFILE *p) if (v == NULL) break; + // empty tuples are untracked, and we can check if n > 0, + // but using PyObject_GC_UnTrack is clearer + PyObject_GC_UnTrack(v); + track_tuple = false; + for (i = 0; i < n; i++) { v2 = r_object(p); if ( v2 == NULL ) { @@ -1429,11 +1435,17 @@ r_object(RFILE *p) PyErr_SetString(PyExc_TypeError, "NULL object in marshal data for tuple"); Py_SETREF(v, NULL); + track_tuple = false; break; } PyTuple_SET_ITEM(v, i, v2); + if (!track_tuple && PyObject_GC_IsTracked(v2)) { + track_tuple = true; + } + } + if (track_tuple) { + _PyObject_GC_TRACK(v); } - _PyTuple_MaybeUntrack(v); retval = v; break;