https://github.com/python/cpython/commit/e1d969ad95c21cccd9b2d47d4ce9c40343aa7b6d commit: e1d969ad95c21cccd9b2d47d4ce9c40343aa7b6d branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-08-29T19:01:09+03:00 summary:
gh-148653: Forbid marshalling recursive tuples (GH-155903) Also fix a crash when unmarshalling a self-referencing tuple. Co-authored-by: Michael Bommarito <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> files: A Misc/NEWS.d/next/Core_and_Builtins/2026-08-16-19-33-01.gh-issue-148653.Kt3vQx.rst M Lib/test/test_marshal.py M Python/marshal.c diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 0e71d65f22b4f0..b5bacadbfd381f 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -342,14 +342,26 @@ def test_reference_loop_dict(self): def test_reference_loop_tuple(self): a = ([],) a[0].append(a) - for v in range(3): + for v in range(marshal.version + 1): self.assertRaises(ValueError, marshal.dumps, a, v) + + a = ({},) + a[0][None] = a + for v in range(marshal.version + 1): + self.assertRaises(ValueError, marshal.dumps, a, v) + + def test_shared_reference_tuple(self): + # A tuple referenced more than once still round-trips with the + # shared identity preserved. + a = (1, 2) for v in range(3, marshal.version + 1): - d = marshal.dumps(a, v) - b = marshal.loads(d) - self.assertIsInstance(b, tuple) - self.assertIsInstance(b[0], list) - self.assertIs(b[0][0], b) + b = marshal.loads(marshal.dumps([a, a], v)) + self.assertEqual(b[0], a) + self.assertIs(b[0], b[1]) + big = tuple(range(300)) # too large for TYPE_SMALL_TUPLE + b = marshal.loads(marshal.dumps([big, big])) + self.assertEqual(b[0], big) + self.assertIs(b[0], b[1]) def test_reference_loop_code(self): def f(): @@ -409,27 +421,6 @@ def test_loads_reference_loop_dict(self): self.assertIs(a[None], a) def test_loads_abnormal_reference_loops(self): - # Indirect self-references of tuples. - data = b'\xa8\x01\x00\x00\x00[\x01\x00\x00\x00r\x00\x00\x00\x00' # ([<R>],) - a = marshal.loads(data) - self.assertIsInstance(a, tuple) - self.assertIsInstance(a[0], list) - self.assertIs(a[0][0], a) - - data = b'\xa8\x01\x00\x00\x00{Nr\x00\x00\x00\x000' # ({None: <R>},) - a = marshal.loads(data) - self.assertIsInstance(a, tuple) - self.assertIsInstance(a[0], dict) - self.assertIs(a[0][None], a) - - # Direct self-reference which cannot be created in Python. - # This creates a reference loop which cannot be collected. - if False: - data = b'\xa8\x01\x00\x00\x00r\x00\x00\x00\x00' # (<R>,) - a = marshal.loads(data) - self.assertIsInstance(a, tuple) - self.assertIs(a[0], a) - # Direct self-references which cannot be created in Python # because of unhashability. data = b'\xfbr\x00\x00\x00\x00N0' # {<R>: None} @@ -439,6 +430,8 @@ def test_loads_abnormal_reference_loops(self): for data in [ # Indirect self-references of immutable objects. + b'\xa8\x01\x00\x00\x00[\x01\x00\x00\x00r\x00\x00\x00\x00', # ([<R>],) + b'\xa8\x01\x00\x00\x00{Nr\x00\x00\x00\x000', # ({None: <R>},) b'\xba[\x01\x00\x00\x00r\x00\x00\x00\x00NN', # slice([<R>], None) b'\xbaN[\x01\x00\x00\x00r\x00\x00\x00\x00N', # slice(None, [<R>]) b'\xbaNN[\x01\x00\x00\x00r\x00\x00\x00\x00', # slice(None, None, [<R>]) @@ -449,12 +442,18 @@ def test_loads_abnormal_reference_loops(self): b'\xfdN{Nr\x00\x00\x00\x0000', # frozendict({None: {None: <R>}) # Direct self-references which cannot be created in Python. + b'\xa8\x01\x00\x00\x00r\x00\x00\x00\x00', # (<R>,) b'\xbe\x01\x00\x00\x00r\x00\x00\x00\x00', # frozenset({<R>}) b'\xfdNr\x00\x00\x00\x000', # frozendict({None: <R>}) b'\xfdr\x00\x00\x00\x00N0', # frozendict({<R>: None}) b'\xbar\x00\x00\x00\x00NN', # slice(<R>, None) b'\xbaNr\x00\x00\x00\x00N', # slice(None, <R>) b'\xbaNNr\x00\x00\x00\x00', # slice(None, None, <R>) + + # Indirect self-references which cannot be created in Python + # because of unhashability. + b'\xa8\x01\x00\x00\x00{r\x00\x00\x00\x00N0', # ({<R>: None},) + b'\xa8\x01\x00\x00\x00<\x01\x00\x00\x00r\x00\x00\x00\x00', # ({<R>},) ]: with self.subTest(data=data): self.assertRaises(ValueError, marshal.loads, data) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-16-19-33-01.gh-issue-148653.Kt3vQx.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-16-19-33-01.gh-issue-148653.Kt3vQx.rst new file mode 100644 index 00000000000000..78502cc4c6a747 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-16-19-33-01.gh-issue-148653.Kt3vQx.rst @@ -0,0 +1,2 @@ +Forbid :mod:`marshalling <marshal>` recursive tuples, and fix a crash when +unmarshalling a self-referencing tuple. diff --git a/Python/marshal.c b/Python/marshal.c index b11f2dca57a226..ef5a8d3840cd80 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -417,7 +417,9 @@ w_ref(PyObject *v, char *flag, WFILE *p) } // Corresponding code should call w_complete() after // writing the object. - if (PyCode_Check(v) || PySlice_Check(v) || PyFrozenDict_CheckExact(v)) { + if (PyTuple_CheckExact(v) || PyCode_Check(v) || PySlice_Check(v) || + PyFrozenDict_CheckExact(v)) + { w |= 0x80000000LU; } if (_Py_hashtable_set(p->hashtable, Py_NewRef(v), @@ -596,6 +598,7 @@ w_complex_object(PyObject *v, char flag, WFILE *p) for (i = 0; i < n; i++) { w_object(PyTuple_GET_ITEM(v, i), p); } + w_complete(v, p); } else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); @@ -1417,8 +1420,10 @@ r_object(RFILE *p) break; } _read_tuple: + idx = r_ref_reserve(flag, p); + if (idx < 0) + break; v = PyTuple_New(n); - R_REF(v); if (v == NULL) break; @@ -1433,7 +1438,7 @@ r_object(RFILE *p) } PyTuple_SET_ITEM(v, i, v2); } - retval = v; + retval = r_ref_insert(v, idx, flag, p); break; case TYPE_LIST: _______________________________________________ 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]
