Skip to content

Commit 8c7ac57

Browse files
authored
[3.15] gh-159184: Do not compare set elements when marshalling a set (GH-159196) (#159205)
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 9ad8605 commit 8c7ac57

3 files changed

Lines changed: 65 additions & 13 deletions

File tree

‎Lib/test/test_marshal.py‎

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

585+
def test_unorderable_set_items(self):
586+
# Set items are sorted by their marshalled representation, they
587+
# are not compared, even if NaNs which are distinct objects have
588+
# the same marshalled representation.
589+
nan = float('nan')
590+
for kind in set, frozenset:
591+
for version in range(marshal.version + 1):
592+
with self.subTest(kind=kind, version=version):
593+
sample = kind([complex(nan, 0), complex(nan, 0), 1j,
594+
(complex(nan, 0),), (complex(nan, 0),),
595+
float('nan'), float('nan')])
596+
self.assertEqual(len(sample), 7)
597+
new = marshal.loads(marshal.dumps(sample, version))
598+
self.assertIs(type(new), kind)
599+
self.assertEqual(sorted(map(repr, new)),
600+
sorted(map(repr, sample)))
601+
602+
def test_unorderable_set_items_in_code(self):
603+
# A constant set containing NaNs which are distinct objects.
604+
# Run in a subprocess: constants of code objects are immortal
605+
# in the free-threaded build.
606+
script = textwrap.dedent("""
607+
import marshal
608+
source = 'x in {(1e999-1e999)*1j, (1e999-1e999)*2j, 1.5}'
609+
code = compile(source, '<marshal test>', 'eval')
610+
def consts(code):
611+
return [sorted(map(repr, x)) if isinstance(x, frozenset) else x
612+
for x in code.co_consts]
613+
assert ['(nan+nanj)', '(nan+nanj)', '1.5'] in consts(code)
614+
for version in range(marshal.version + 1):
615+
new = marshal.loads(marshal.dumps(code, version))
616+
assert new.co_code == code.co_code
617+
assert consts(new) == consts(code)
618+
assert eval(new, {'x': 1.5}) is True
619+
assert eval(new, {'x': complex('nan')}) is False
620+
""")
621+
assert_python_ok('-c', script)
622+
585623
def test_set_changed_size(self):
586624
# A set is modified while its items are being marshalled.
587625
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: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,13 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
646646
// bpo-37596: To support reproducible builds, sets and frozensets need
647647
// to have their elements serialized in a consistent order (even when
648648
// they have been scrambled by hash randomization). To ensure this, we
649-
// use an order equivalent to sorted(v, key=marshal.dumps):
650-
PyObject *pairs = PyList_New(n);
651-
if (pairs == NULL) {
649+
// use an order equivalent to sorted(v, key=marshal.dumps).
650+
// The list has a (dump, index, element) tuple for each element.
651+
// The indexes are all different, so that sorting never compares
652+
// two elements: elements with the same dump (NaNs which are
653+
// distinct objects) may not be orderable.
654+
PyObject *items = PyList_New(n);
655+
if (items == NULL) {
652656
p->error = WFERR_NOMEMORY;
653657
return;
654658
}
@@ -672,34 +676,41 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
672676
Py_DECREF(value);
673677
break;
674678
}
675-
PyObject *pair = _PyTuple_FromPairSteal(dump, value);
676-
if (pair == NULL) {
679+
PyObject *index = PyLong_FromSsize_t(i);
680+
PyObject *item = NULL;
681+
if (index != NULL) {
682+
item = PyTuple_Pack(3, dump, index, value);
683+
Py_DECREF(index);
684+
}
685+
Py_DECREF(dump);
686+
Py_DECREF(value);
687+
if (item == NULL) {
677688
p->error = WFERR_NOMEMORY;
678689
break;
679690
}
680-
PyList_SET_ITEM(pairs, i++, pair);
691+
PyList_SET_ITEM(items, i++, item);
681692
}
682693
Py_END_CRITICAL_SECTION();
683694
if (p->error == WFERR_UNMARSHALLABLE || p->error == WFERR_NOMEMORY) {
684-
Py_DECREF(pairs);
695+
Py_DECREF(items);
685696
return;
686697
}
687698
if (changed || i != n) {
688699
p->error = WFERR_SET_CHANGED_SIZE;
689-
Py_DECREF(pairs);
700+
Py_DECREF(items);
690701
return;
691702
}
692-
if (PyList_Sort(pairs)) {
703+
if (PyList_Sort(items)) {
693704
p->error = WFERR_NOMEMORY;
694-
Py_DECREF(pairs);
705+
Py_DECREF(items);
695706
return;
696707
}
697708
for (Py_ssize_t i = 0; i < n; i++) {
698-
PyObject *pair = PyList_GET_ITEM(pairs, i);
699-
value = PyTuple_GET_ITEM(pair, 1);
709+
PyObject *item = PyList_GET_ITEM(items, i);
710+
value = PyTuple_GET_ITEM(item, 2);
700711
w_object(value, p);
701712
}
702-
Py_DECREF(pairs);
713+
Py_DECREF(items);
703714
}
704715
else if (PyCode_Check(v)) {
705716
if (!p->allow_code) {

0 commit comments

Comments
 (0)