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.
This commit is contained in:
Serhiy Storchaka 2023-09-10 20:06:09 +03:00 committed by GitHub
parent 71b6e2602c
commit d6892c2b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 0 deletions

View File

@ -414,6 +414,9 @@ def __enter__(self):
def __exit__(self, type, value, tb): def __exit__(self, type, value, tb):
self.stream.close() self.stream.close()
def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)
### ###
class StreamReader(Codec): class StreamReader(Codec):
@ -663,6 +666,9 @@ def __enter__(self):
def __exit__(self, type, value, tb): def __exit__(self, type, value, tb):
self.stream.close() self.stream.close()
def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)
### ###
class StreamReaderWriter: class StreamReaderWriter:
@ -750,6 +756,9 @@ def __enter__(self):
def __exit__(self, type, value, tb): def __exit__(self, type, value, tb):
self.stream.close() self.stream.close()
def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)
### ###
class StreamRecoder: class StreamRecoder:
@ -866,6 +875,9 @@ def __enter__(self):
def __exit__(self, type, value, tb): def __exit__(self, type, value, tb):
self.stream.close() self.stream.close()
def __reduce_ex__(self, proto):
raise TypeError("can't serialize %s" % self.__class__.__name__)
### Shortcuts ### Shortcuts
def open(filename, mode='r', encoding=None, errors='strict', buffering=-1): def open(filename, mode='r', encoding=None, errors='strict', buffering=-1):

View File

@ -1,7 +1,9 @@
import codecs import codecs
import contextlib import contextlib
import copy
import io import io
import locale import locale
import pickle
import sys import sys
import unittest import unittest
import encodings import encodings
@ -1771,6 +1773,61 @@ def test_readlines(self):
f = self.reader(self.stream) f = self.reader(self.stream)
self.assertEqual(f.readlines(), ['\ud55c\n', '\uae00']) 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): class EncodedFileTest(unittest.TestCase):
@ -3346,6 +3403,28 @@ def test_seeking_write(self):
self.assertEqual(sr.readline(), b'abc\n') self.assertEqual(sr.readline(), b'abc\n')
self.assertEqual(sr.readline(), b'789\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') @unittest.skipIf(_testinternalcapi is None, 'need _testinternalcapi module')
class LocaleCodecTest(unittest.TestCase): class LocaleCodecTest(unittest.TestCase):

View File

@ -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.