Skip to content

Commit b070d73

Browse files
[3.11] gh-50644: Forbid pickling of codecs streams (GH-109180) (GH-109232)
Attempts to pickle or create a shallow or deep copy of codecs streams now raise a TypeError. Previously, copying failed with a RecursionError, while pickling produced wrong results that eventually caused unpickling to fail with a RecursionError. (cherry picked from commit d6892c2) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent c206582 commit b070d73

File tree

3 files changed

+95
-0
lines changed

3 files changed

+95
-0
lines changed

Lib/codecs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ def __enter__(self):
414414
def __exit__(self, type, value, tb):
415415
self.stream.close()
416416

417+
def __reduce_ex__(self, proto):
418+
raise TypeError("can't serialize %s" % self.__class__.__name__)
419+
417420
###
418421

419422
class StreamReader(Codec):
@@ -663,6 +666,9 @@ def __enter__(self):
663666
def __exit__(self, type, value, tb):
664667
self.stream.close()
665668

669+
def __reduce_ex__(self, proto):
670+
raise TypeError("can't serialize %s" % self.__class__.__name__)
671+
666672
###
667673

668674
class StreamReaderWriter:
@@ -750,6 +756,9 @@ def __enter__(self):
750756
def __exit__(self, type, value, tb):
751757
self.stream.close()
752758

759+
def __reduce_ex__(self, proto):
760+
raise TypeError("can't serialize %s" % self.__class__.__name__)
761+
753762
###
754763

755764
class StreamRecoder:
@@ -866,6 +875,9 @@ def __enter__(self):
866875
def __exit__(self, type, value, tb):
867876
self.stream.close()
868877

878+
def __reduce_ex__(self, proto):
879+
raise TypeError("can't serialize %s" % self.__class__.__name__)
880+
869881
### Shortcuts
870882

871883
def open(filename, mode='r', encoding=None, errors='strict', buffering=-1):

Lib/test/test_codecs.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import codecs
22
import contextlib
3+
import copy
34
import io
45
import locale
6+
import pickle
57
import sys
68
import unittest
79
import encodings
@@ -1772,6 +1774,61 @@ def test_readlines(self):
17721774
f = self.reader(self.stream)
17731775
self.assertEqual(f.readlines(), ['\ud55c\n', '\uae00'])
17741776

1777+
def test_copy(self):
1778+
f = self.reader(Queue(b'\xed\x95\x9c\n\xea\xb8\x80'))
1779+
with self.assertRaisesRegex(TypeError, 'StreamReader'):
1780+
copy.copy(f)
1781+
with self.assertRaisesRegex(TypeError, 'StreamReader'):
1782+
copy.deepcopy(f)
1783+
1784+
def test_pickle(self):
1785+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1786+
with self.subTest(protocol=proto):
1787+
f = self.reader(Queue(b'\xed\x95\x9c\n\xea\xb8\x80'))
1788+
with self.assertRaisesRegex(TypeError, 'StreamReader'):
1789+
pickle.dumps(f, proto)
1790+
1791+
1792+
class StreamWriterTest(unittest.TestCase):
1793+
1794+
def setUp(self):
1795+
self.writer = codecs.getwriter('utf-8')
1796+
1797+
def test_copy(self):
1798+
f = self.writer(Queue(b''))
1799+
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
1800+
copy.copy(f)
1801+
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
1802+
copy.deepcopy(f)
1803+
1804+
def test_pickle(self):
1805+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1806+
with self.subTest(protocol=proto):
1807+
f = self.writer(Queue(b''))
1808+
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
1809+
pickle.dumps(f, proto)
1810+
1811+
1812+
class StreamReaderWriterTest(unittest.TestCase):
1813+
1814+
def setUp(self):
1815+
self.reader = codecs.getreader('latin1')
1816+
self.writer = codecs.getwriter('utf-8')
1817+
1818+
def test_copy(self):
1819+
f = codecs.StreamReaderWriter(Queue(b''), self.reader, self.writer)
1820+
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
1821+
copy.copy(f)
1822+
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
1823+
copy.deepcopy(f)
1824+
1825+
def test_pickle(self):
1826+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1827+
with self.subTest(protocol=proto):
1828+
f = codecs.StreamReaderWriter(Queue(b''), self.reader, self.writer)
1829+
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
1830+
pickle.dumps(f, proto)
1831+
17751832

17761833
class EncodedFileTest(unittest.TestCase):
17771834

@@ -3369,6 +3426,28 @@ def test_seeking_write(self):
33693426
self.assertEqual(sr.readline(), b'abc\n')
33703427
self.assertEqual(sr.readline(), b'789\n')
33713428

3429+
def test_copy(self):
3430+
bio = io.BytesIO()
3431+
codec = codecs.lookup('ascii')
3432+
sr = codecs.StreamRecoder(bio, codec.encode, codec.decode,
3433+
encodings.ascii.StreamReader, encodings.ascii.StreamWriter)
3434+
3435+
with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
3436+
copy.copy(sr)
3437+
with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
3438+
copy.deepcopy(sr)
3439+
3440+
def test_pickle(self):
3441+
q = Queue(b'')
3442+
codec = codecs.lookup('ascii')
3443+
sr = codecs.StreamRecoder(q, codec.encode, codec.decode,
3444+
encodings.ascii.StreamReader, encodings.ascii.StreamWriter)
3445+
3446+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
3447+
with self.subTest(protocol=proto):
3448+
with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
3449+
pickle.dumps(sr, proto)
3450+
33723451

33733452
@unittest.skipIf(_testinternalcapi is None, 'need _testinternalcapi module')
33743453
class LocaleCodecTest(unittest.TestCase):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Attempts to pickle or create a shallow or deep copy of :mod:`codecs` streams
2+
now raise a TypeError. Previously, copying failed with a RecursionError,
3+
while pickling produced wrong results that eventually caused unpickling
4+
to fail with a RecursionError.

0 commit comments

Comments
 (0)