diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 9c4d91c456dc5d9..b612979995f1c04 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(): @@ -399,27 +411,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' # ([],) - 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: },) - 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' # (,) - 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' # {: None} @@ -429,6 +420,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', # ([],) + b'\xa8\x01\x00\x00\x00{Nr\x00\x00\x00\x000', # ({None: },) b'\xba[\x01\x00\x00\x00r\x00\x00\x00\x00NN', # slice([], None) b'\xbaN[\x01\x00\x00\x00r\x00\x00\x00\x00N', # slice(None, []) b'\xbaNN[\x01\x00\x00\x00r\x00\x00\x00\x00', # slice(None, None, []) @@ -439,12 +432,18 @@ def test_loads_abnormal_reference_loops(self): b'\xfdN{Nr\x00\x00\x00\x0000', # frozendict({None: {None: }) # Direct self-references which cannot be created in Python. + b'\xa8\x01\x00\x00\x00r\x00\x00\x00\x00', # (,) b'\xbe\x01\x00\x00\x00r\x00\x00\x00\x00', # frozenset({}) b'\xfdNr\x00\x00\x00\x000', # frozendict({None: }) b'\xfdr\x00\x00\x00\x00N0', # frozendict({: None}) b'\xbar\x00\x00\x00\x00NN', # slice(, None) b'\xbaNr\x00\x00\x00\x00N', # slice(None, ) b'\xbaNNr\x00\x00\x00\x00', # slice(None, None, ) + + # Indirect self-references which cannot be created in Python + # because of unhashability. + b'\xa8\x01\x00\x00\x00{r\x00\x00\x00\x00N0', # ({: None},) + b'\xa8\x01\x00\x00\x00<\x01\x00\x00\x00r\x00\x00\x00\x00', # ({},) ]: 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 000000000000000..78502cc4c6a747b --- /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 ` recursive tuples, and fix a crash when +unmarshalling a self-referencing tuple. diff --git a/Python/marshal.c b/Python/marshal.c index 25353f6e6896249..4273be35556941e 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: