Skip to content

Commit 73de0ba

Browse files
gh-143638: Forbid cuncurrent use of the Pickler and Unpickler objects in C implementation
Previously, this could cause crash or data corruption, now concurrent calls of methods of the same object raise RuntimeError.
1 parent e7f5ffa commit 73de0ba

File tree

3 files changed

+193
-72
lines changed

3 files changed

+193
-72
lines changed

Lib/test/test_pickle.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,58 @@ def test_issue18339(self):
419419
unpickler.memo = {-1: None}
420420
unpickler.memo = {1: None}
421421

422+
def test_concurrent_pickler_dump(self):
423+
f = io.BytesIO()
424+
pickler = self.pickler_class(f)
425+
class X:
426+
def __reduce__(slf):
427+
self.assertRaises(RuntimeError, pickler.dump, 42)
428+
return list, ()
429+
pickler.dump(X()) # should not crash
430+
self.assertEqual(pickle.loads(f.getvalue()), [])
431+
432+
def test_concurrent_pickler_dump_and_clear_memo(self):
433+
for clear_memo in [lambda: pickler.clear_memo(), lambda: pickler.memo.clear()]:
434+
f = io.BytesIO()
435+
pickler = self.pickler_class(f)
436+
class X:
437+
def __reduce__(slf):
438+
self.assertRaises(RuntimeError, clear_memo)
439+
return list, (('inner',),), None, iter((slf,))
440+
pickler.dump(['outer', X()])
441+
unpickled = pickle.loads(f.getvalue())
442+
self.assertIs(unpickled[1][1], unpickled[1])
443+
444+
def test_concurrent_pickler_dump_and_init(self):
445+
f = io.BytesIO()
446+
pickler = self.pickler_class(f)
447+
class X:
448+
def __reduce__(slf):
449+
self.assertRaises(RuntimeError, pickler.__init__, f)
450+
return list, ()
451+
pickler.dump([X()]) # should not fail
452+
self.assertEqual(pickle.loads(f.getvalue()), [[]])
453+
454+
def test_concurrent_unpickler_load(self):
455+
global reducer
456+
def reducer():
457+
self.assertRaises(RuntimeError, unpickler.load)
458+
return 42
459+
f = io.BytesIO(b'(c%b\nreducer\n(tRl.' % (__name__.encode(),))
460+
unpickler = self.unpickler_class(f)
461+
unpickled = unpickler.load() # should not fail
462+
self.assertEqual(unpickled, [42])
463+
464+
def test_concurrent_unpickler_load_and_init(self):
465+
global reducer
466+
def reducer():
467+
self.assertRaises(RuntimeError, unpickler.__init__, f)
468+
return 42
469+
f = io.BytesIO(b'(c%b\nreducer\n(tRl.' % (__name__.encode(),))
470+
unpickler = self.unpickler_class(f)
471+
unpickled = unpickler.load() # should not crash
472+
self.assertEqual(unpickled, [42])
473+
422474
class CDispatchTableTests(AbstractDispatchTableTests, unittest.TestCase):
423475
pickler_class = pickle.Pickler
424476
def get_dispatch_table(self):
@@ -467,7 +519,7 @@ class SizeofTests(unittest.TestCase):
467519
check_sizeof = support.check_sizeof
468520

469521
def test_pickler(self):
470-
basesize = support.calcobjsize('7P2n3i2n3i2P')
522+
basesize = support.calcobjsize('7P2n3i2n4i2P')
471523
p = _pickle.Pickler(io.BytesIO())
472524
self.assertEqual(object.__sizeof__(p), basesize)
473525
MT_size = struct.calcsize('3nP0n')
@@ -484,7 +536,7 @@ def test_pickler(self):
484536
0) # Write buffer is cleared after every dump().
485537

486538
def test_unpickler(self):
487-
basesize = support.calcobjsize('2P2n3P 2P2n2i5P 2P3n8P2n2i')
539+
basesize = support.calcobjsize('2P2n3P 2P2n2i5P 2P3n8P2n3i')
488540
unpickler = _pickle.Unpickler
489541
P = struct.calcsize('P') # Size of memo table entry.
490542
n = struct.calcsize('n') # Size of mark table entry.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Forbid reentrant calls of the :class:`pickle.Pickler` and
2+
:class:`pickle.Unpickler` methods for the C implementation. Previously, this
3+
could cause crash or data corruption, now concurrent calls of methods of the
4+
same object raise :exc:`RuntimeError`.

0 commit comments

Comments
 (0)