Skip to content

Commit 407a473

Browse files
committed
gh-106287: do not write objects after encountering an unmarshalling error
Writing out an object may involve a slot lookup, which is not safe to do with an exception raised. In debug mode an assertion failure will occur if this happens. To avoid assertion failures, and possibly more subtle problems the assertion is there to prevent, skip attempts to write an object after an error. Note the unmarshal writing code won't raise an exception itself, however the set writing code calls back into the top-level unmarshal function (see gh-78903), which will check for failures and raises an exception. Once that happens it is not safe to continue writing further objects. Note also that some data may still be written even after an error occurred and an exception has been raised: code objects write objects and longs, and the latter will be written unconditionally, even if the former fails. This shouldn't cause a problem as it is documented that "garbage data will also be written to the file" in such cases, and the top-level function will handle and report the error appropriately. Add a test case to cover the various different object types which could trigger such failures.
1 parent 815061c commit 407a473

File tree

2 files changed

+26
-0
lines changed

2 files changed

+26
-0
lines changed

Lib/test/test_marshal.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,25 @@ def test_deterministic_sets(self):
408408
_, dump_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1")
409409
self.assertEqual(dump_0, dump_1)
410410

411+
def test_unmarshallable(self):
412+
# gh-106287
413+
fset = frozenset([int])
414+
code = compile("a = 1", "<string>", "exec")
415+
code = code.replace(co_consts=(1, fset, None))
416+
cases = (('tuple', (fset,)),
417+
('list', [fset]),
418+
('set', fset),
419+
('dict key', {fset: 'x'}),
420+
('dict value', {'x': fset}),
421+
('dict key & value', {fset: fset}),
422+
('dict set key & buffer', {fset: fset}),
423+
('slice', slice(fset, fset)),
424+
('code', code))
425+
for name, arg in cases:
426+
with self.subTest(name, arg=arg):
427+
with self.assertRaisesRegex(ValueError, "unmarshallable object"):
428+
marshal.dumps((arg, memoryview(b'')))
429+
411430
LARGE_SIZE = 2**31
412431
pointer_size = 8 if sys.maxsize > 0xFFFFFFFF else 4
413432

Python/marshal.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,9 @@ w_object(PyObject *v, WFILE *p)
431431
{
432432
char flag = '\0';
433433

434+
if (p->error != WFERR_OK)
435+
return;
436+
434437
p->depth++;
435438

436439
if (p->depth > MAX_MARSHAL_STACK_DEPTH) {
@@ -750,6 +753,10 @@ PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
750753
w_flush(&wf);
751754
}
752755

756+
/* TODO: Contrary to its documentation, this function does NOT set an error on
757+
* failure. It is not used internally except from test code, where this doesn't
758+
* matter, but this discrepancy might cause problems for any external code
759+
* using it. */
753760
void
754761
PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
755762
{

0 commit comments

Comments
 (0)