diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index c595e8cf14f1e1..7ab2a3f80244a0 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -2,6 +2,7 @@ from test.support import is_apple_mobile, os_helper, requires_debug_ranges, is_emscripten from test.support.script_helper import assert_python_ok import array +import gc import io import marshal import sys @@ -900,6 +901,56 @@ 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", [] + t = (True, False, ()) + + 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, t)) + + self._tracked(([],)) + self._tracked(([1],)) + self._tracked(({},)) + self._tracked((set(),)) + self._tracked((x, y, z)) + if __name__ == "__main__": unittest.main() 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. diff --git a/Python/marshal.c b/Python/marshal.c index 1897d700c055bd..68396da40e2eda 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -1260,6 +1260,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)) { @@ -1504,6 +1505,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 ) { @@ -1511,9 +1517,16 @@ 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); } retval = r_ref_insert(v, idx, flag, p); break;