Skip to content

Commit

Permalink
[3.12] gh-50644: Forbid pickling of codecs streams (GH-109180) (#109231)
Browse files Browse the repository at this point in the history
gh-50644: Forbid pickling of codecs streams (GH-109180)

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]>
  • Loading branch information
miss-islington and serhiy-storchaka authored Oct 2, 2023
1 parent be8255a commit 3e1c9e8
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Lib/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ def __enter__(self):
def __exit__(self, type, value, tb):
self.stream.close()

def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)

###

class StreamReader(Codec):
Expand Down Expand Up @@ -663,6 +666,9 @@ def __enter__(self):
def __exit__(self, type, value, tb):
self.stream.close()

def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)

###

class StreamReaderWriter:
Expand Down Expand Up @@ -750,6 +756,9 @@ def __enter__(self):
def __exit__(self, type, value, tb):
self.stream.close()

def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)

###

class StreamRecoder:
Expand Down Expand Up @@ -866,6 +875,9 @@ def __enter__(self):
def __exit__(self, type, value, tb):
self.stream.close()

def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)

### Shortcuts

def open(filename, mode='r', encoding=None, errors='strict', buffering=-1):
Expand Down
79 changes: 79 additions & 0 deletions Lib/test/test_codecs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import codecs
import contextlib
import copy
import io
import locale
import pickle
import sys
import unittest
import encodings
Expand Down Expand Up @@ -1771,6 +1773,61 @@ def test_readlines(self):
f = self.reader(self.stream)
self.assertEqual(f.readlines(), ['\ud55c\n', '\uae00'])

def test_copy(self):
f = self.reader(Queue(b'\xed\x95\x9c\n\xea\xb8\x80'))
with self.assertRaisesRegex(TypeError, 'StreamReader'):
copy.copy(f)
with self.assertRaisesRegex(TypeError, 'StreamReader'):
copy.deepcopy(f)

def test_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
f = self.reader(Queue(b'\xed\x95\x9c\n\xea\xb8\x80'))
with self.assertRaisesRegex(TypeError, 'StreamReader'):
pickle.dumps(f, proto)


class StreamWriterTest(unittest.TestCase):

def setUp(self):
self.writer = codecs.getwriter('utf-8')

def test_copy(self):
f = self.writer(Queue(b''))
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
copy.copy(f)
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
copy.deepcopy(f)

def test_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
f = self.writer(Queue(b''))
with self.assertRaisesRegex(TypeError, 'StreamWriter'):
pickle.dumps(f, proto)


class StreamReaderWriterTest(unittest.TestCase):

def setUp(self):
self.reader = codecs.getreader('latin1')
self.writer = codecs.getwriter('utf-8')

def test_copy(self):
f = codecs.StreamReaderWriter(Queue(b''), self.reader, self.writer)
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
copy.copy(f)
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
copy.deepcopy(f)

def test_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
f = codecs.StreamReaderWriter(Queue(b''), self.reader, self.writer)
with self.assertRaisesRegex(TypeError, 'StreamReaderWriter'):
pickle.dumps(f, proto)


class EncodedFileTest(unittest.TestCase):

Expand Down Expand Up @@ -3346,6 +3403,28 @@ def test_seeking_write(self):
self.assertEqual(sr.readline(), b'abc\n')
self.assertEqual(sr.readline(), b'789\n')

def test_copy(self):
bio = io.BytesIO()
codec = codecs.lookup('ascii')
sr = codecs.StreamRecoder(bio, codec.encode, codec.decode,
encodings.ascii.StreamReader, encodings.ascii.StreamWriter)

with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
copy.copy(sr)
with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
copy.deepcopy(sr)

def test_pickle(self):
q = Queue(b'')
codec = codecs.lookup('ascii')
sr = codecs.StreamRecoder(q, codec.encode, codec.decode,
encodings.ascii.StreamReader, encodings.ascii.StreamWriter)

for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
with self.assertRaisesRegex(TypeError, 'StreamRecoder'):
pickle.dumps(sr, proto)


@unittest.skipIf(_testinternalcapi is None, 'need _testinternalcapi module')
class LocaleCodecTest(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Attempts to pickle or create a shallow or deep copy of :mod:`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.

0 comments on commit 3e1c9e8

Please sign in to comment.