https://github.com/python/cpython/commit/1967a87ad395552dbd1e16fd3f13d3b4b2f694de
commit: 1967a87ad395552dbd1e16fd3f13d3b4b2f694de
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-11T15:53:04-07:00
summary:

gh-159184: Do not compare set elements when marshalling a set (GH-159196)

Set elements are written in the order of their dumps.  They were sorted
as (dump, element) pairs, so elements with the same dump were compared.
That raised TypeError for a set of several complex NaNs, which the
compiler can produce by constant folding, making the module impossible
to marshal.

Sort (dump, index, element) tuples, in which the indexes are all
different.  Elements with the same dump stay in the order of the set,
as float NaNs did before.

files:
A Misc/NEWS.d/next/Library/2026-10-11-08-41-00.gh-issue-159184.Nn4tOr.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 5b4d02c232f4a6..5dd876f7966bed 100644
--- a/Lib/test/test_marshal.py
+++ b/Lib/test/test_marshal.py
@@ -597,13 +597,44 @@ def test_unmarshallable(self):
                                             "cannot marshal type objects"):
                     marshal.dumps((arg, memoryview(b'')))
 
-    def test_error_in_set_item(self):
-        # Set items are sorted by their marshalled representation, and NaNs
-        # are only distinguished by identity, so they are compared as
-        # complex numbers.
+    def test_unorderable_set_items(self):
+        # Set items are sorted by their marshalled representation, they
+        # are not compared, even if NaNs which are distinct objects have
+        # the same marshalled representation.
         nan = float('nan')
-        with self.assertRaisesRegex(TypeError, "'<' not supported"):
-            marshal.dumps({complex(nan, 0), complex(nan, 0)})
+        for kind in set, frozenset:
+            for version in range(marshal.version + 1):
+                with self.subTest(kind=kind, version=version):
+                    sample = kind([complex(nan, 0), complex(nan, 0), 1j,
+                                   (complex(nan, 0),), (complex(nan, 0),),
+                                   float('nan'), float('nan')])
+                    self.assertEqual(len(sample), 7)
+                    new = marshal.loads(marshal.dumps(sample, version))
+                    self.assertIs(type(new), kind)
+                    self.assertEqual(sorted(map(repr, new)),
+                                     sorted(map(repr, sample)))
+
+
+    def test_unorderable_set_items_in_code(self):
+        # A constant set containing NaNs which are distinct objects.
+        # Run in a subprocess: constants of code objects are immortal
+        # in the free-threaded build.
+        script = textwrap.dedent("""
+            import marshal
+            source = 'x in {(1e999-1e999)*1j, (1e999-1e999)*2j, 1.5}'
+            code = compile(source, '<marshal test>', 'eval')
+            def consts(code):
+                return [sorted(map(repr, x)) if isinstance(x, frozenset) else x
+                        for x in code.co_consts]
+            assert ['(nan+nanj)', '(nan+nanj)', '1.5'] in consts(code)
+            for version in range(marshal.version + 1):
+                new = marshal.loads(marshal.dumps(code, version))
+                assert new.co_code == code.co_code
+                assert consts(new) == consts(code)
+                assert eval(new, {'x': 1.5}) is True
+                assert eval(new, {'x': complex('nan')}) is False
+            """)
+        assert_python_ok('-c', script)
 
     def test_set_changed_size(self):
         # A set is modified while its items are being marshalled.
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-11-08-41-00.gh-issue-159184.Nn4tOr.rst 
b/Misc/NEWS.d/next/Library/2026-10-11-08-41-00.gh-issue-159184.Nn4tOr.rst
new file mode 100644
index 00000000000000..014b95569cdb28
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-11-08-41-00.gh-issue-159184.Nn4tOr.rst
@@ -0,0 +1,3 @@
+Fix :func:`marshal.dumps` raising :exc:`TypeError` for a set or frozenset
+containing several complex NaNs, which also made it impossible to write a
+:file:`.pyc` file for source code containing such a constant.
diff --git a/Python/marshal.c b/Python/marshal.c
index b6a50627854eaf..cb5833d9c6e0b0 100644
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -675,9 +675,13 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
         // bpo-37596: To support reproducible builds, sets and frozensets need
         // to have their elements serialized in a consistent order (even when
         // they have been scrambled by hash randomization). To ensure this, we
-        // use an order equivalent to sorted(v, key=marshal.dumps):
-        PyObject *pairs = PyList_New(n);
-        if (pairs == NULL) {
+        // use an order equivalent to sorted(v, key=marshal.dumps).
+        // The list has a (dump, index, element) tuple for each element.
+        // The indexes are all different, so that sorting never compares
+        // two elements: elements with the same dump (NaNs which are
+        // distinct objects) may not be orderable.
+        PyObject *items = PyList_New(n);
+        if (items == NULL) {
             p->error = true;
             return;
         }
@@ -701,12 +705,19 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
                 Py_DECREF(value);
                 break;
             }
-            PyObject *pair = _PyTuple_FromPairSteal(dump, value);
-            if (pair == NULL) {
+            PyObject *index = PyLong_FromSsize_t(i);
+            PyObject *item = NULL;
+            if (index != NULL) {
+                item = PyTuple_Pack(3, dump, index, value);
+                Py_DECREF(index);
+            }
+            Py_DECREF(dump);
+            Py_DECREF(value);
+            if (item == NULL) {
                 p->error = true;
                 break;
             }
-            PyList_SET_ITEM(pairs, i++, pair);
+            PyList_SET_ITEM(items, i++, item);
         }
         Py_END_CRITICAL_SECTION();
         if (!p->error && (changed || i != n)) {
@@ -715,20 +726,20 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
             p->error = true;
         }
         if (p->error) {
-            Py_DECREF(pairs);
+            Py_DECREF(items);
             return;
         }
-        if (PyList_Sort(pairs)) {
+        if (PyList_Sort(items)) {
             p->error = true;
-            Py_DECREF(pairs);
+            Py_DECREF(items);
             return;
         }
         for (Py_ssize_t i = 0; i < n; i++) {
-            PyObject *pair = PyList_GET_ITEM(pairs, i);
-            value = PyTuple_GET_ITEM(pair, 1);
+            PyObject *item = PyList_GET_ITEM(items, i);
+            value = PyTuple_GET_ITEM(item, 2);
             w_object(value, p);
         }
-        Py_DECREF(pairs);
+        Py_DECREF(items);
     }
     else if (PyCode_Check(v)) {
         if (!p->allow_code) {

_______________________________________________
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]

Reply via email to