Skip to content

Commit 758278c

Browse files
authored
[3.14] gh-159184: Do not compare set elements when marshalling a set (GH-159196) (#159206)
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 failed for a set of several complex NaNs, which the compiler can produce by constant folding, making the module impossible to marshal. On this branch the failure was reported as MemoryError. 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. (cherry picked from commit 1967a87)
1 parent f8a48fb commit 758278c

3 files changed

Lines changed: 63 additions & 13 deletions

File tree

‎Lib/test/test_marshal.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,44 @@ def test_unmarshallable(self):
562562
with self.assertRaisesRegex(ValueError, "unmarshallable object"):
563563
marshal.dumps((arg, memoryview(b'')))
564564

565+
def test_unorderable_set_items(self):
566+
# Set items are sorted by their marshalled representation, they
567+
# are not compared, even if NaNs which are distinct objects have
568+
# the same marshalled representation.
569+
nan = float('nan')
570+
for kind in set, frozenset:
571+
for version in range(marshal.version + 1):
572+
with self.subTest(kind=kind, version=version):
573+
sample = kind([complex(nan, 0), complex(nan, 0), 1j,
574+
(complex(nan, 0),), (complex(nan, 0),),
575+
float('nan'), float('nan')])
576+
self.assertEqual(len(sample), 7)
577+
new = marshal.loads(marshal.dumps(sample, version))
578+
self.assertIs(type(new), kind)
579+
self.assertEqual(sorted(map(repr, new)),
580+
sorted(map(repr, sample)))
581+
582+
def test_unorderable_set_items_in_code(self):
583+
# A constant set containing NaNs which are distinct objects.
584+
# Run in a subprocess: constants of code objects are immortal
585+
# in the free-threaded build.
586+
script = textwrap.dedent("""
587+
import marshal
588+
source = 'x in {(1e999-1e999)*1j, (1e999-1e999)*2j, 1.5}'
589+
code = compile(source, '<marshal test>', 'eval')
590+
def consts(code):
591+
return [sorted(map(repr, x)) if isinstance(x, frozenset) else x
592+
for x in code.co_consts]
593+
assert ['(nan+nanj)', '(nan+nanj)', '1.5'] in consts(code)
594+
for version in range(marshal.version + 1):
595+
new = marshal.loads(marshal.dumps(code, version))
596+
assert new.co_code == code.co_code
597+
assert consts(new) == consts(code)
598+
assert eval(new, {'x': 1.5}) is True
599+
assert eval(new, {'x': complex('nan')}) is False
600+
""")
601+
assert_python_ok('-c', script)
602+
565603
def test_set_changed_size(self):
566604
# A set is modified while its items are being marshalled.
567605
class Item:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`marshal.dumps` raising :exc:`MemoryError` for a set or frozenset
2+
containing several complex NaNs, which also made it impossible to write a
3+
:file:`.pyc` file for source code containing such a constant.

‎Python/marshal.c‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -630,9 +630,13 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
630630
// bpo-37596: To support reproducible builds, sets and frozensets need
631631
// to have their elements serialized in a consistent order (even when
632632
// they have been scrambled by hash randomization). To ensure this, we
633-
// use an order equivalent to sorted(v, key=marshal.dumps):
634-
PyObject *pairs = PyList_New(n);
635-
if (pairs == NULL) {
633+
// use an order equivalent to sorted(v, key=marshal.dumps).
634+
// The list has a (dump, index, element) tuple for each element.
635+
// The indexes are all different, so that sorting never compares
636+
// two elements: elements with the same dump (NaNs which are
637+
// distinct objects) may not be orderable.
638+
PyObject *items = PyList_New(n);
639+
if (items == NULL) {
636640
p->error = WFERR_NOMEMORY;
637641
return;
638642
}
@@ -656,36 +660,41 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
656660
Py_DECREF(value);
657661
break;
658662
}
659-
PyObject *pair = PyTuple_Pack(2, dump, value);
663+
PyObject *index = PyLong_FromSsize_t(i);
664+
PyObject *item = NULL;
665+
if (index != NULL) {
666+
item = PyTuple_Pack(3, dump, index, value);
667+
Py_DECREF(index);
668+
}
660669
Py_DECREF(dump);
661670
Py_DECREF(value);
662-
if (pair == NULL) {
671+
if (item == NULL) {
663672
p->error = WFERR_NOMEMORY;
664673
break;
665674
}
666-
PyList_SET_ITEM(pairs, i++, pair);
675+
PyList_SET_ITEM(items, i++, item);
667676
}
668677
Py_END_CRITICAL_SECTION();
669678
if (p->error == WFERR_UNMARSHALLABLE || p->error == WFERR_NOMEMORY) {
670-
Py_DECREF(pairs);
679+
Py_DECREF(items);
671680
return;
672681
}
673682
if (changed || i != n) {
674683
p->error = WFERR_SET_CHANGED_SIZE;
675-
Py_DECREF(pairs);
684+
Py_DECREF(items);
676685
return;
677686
}
678-
if (PyList_Sort(pairs)) {
687+
if (PyList_Sort(items)) {
679688
p->error = WFERR_NOMEMORY;
680-
Py_DECREF(pairs);
689+
Py_DECREF(items);
681690
return;
682691
}
683692
for (Py_ssize_t i = 0; i < n; i++) {
684-
PyObject *pair = PyList_GET_ITEM(pairs, i);
685-
value = PyTuple_GET_ITEM(pair, 1);
693+
PyObject *item = PyList_GET_ITEM(items, i);
694+
value = PyTuple_GET_ITEM(item, 2);
686695
w_object(value, p);
687696
}
688-
Py_DECREF(pairs);
697+
Py_DECREF(items);
689698
}
690699
else if (PyCode_Check(v)) {
691700
if (!p->allow_code) {

0 commit comments

Comments
 (0)