diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index c53262e358b48e..3f6560869f34ac 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -4925,6 +4925,31 @@ def test_load_from_and_dump_to_file(self): unpickled = self.load(stream) self.assertEqual(unpickled, data) + def test_load_from_file_returning_bytes_subclass(self): + # gh-158841: read() returning a bytes subclass must not crash + # when the payload is large enough to need several chunked reads. + class MyBytes(bytes): + pass + + class Reader: + def __init__(self, data): + self.data = data + self.pos = 0 + def read(self, n): + chunk = self.data[self.pos:self.pos + n] + self.pos += len(chunk) + return MyBytes(chunk) + def readline(self): + end = self.data.find(b'\n', self.pos) + end = len(self.data) if end < 0 else end + 1 + line = self.data[self.pos:end] + self.pos = end + return line + + obj = {'v': 'B' * (3 * 1024 * 1024)} + data = self.dumps(obj, protocol=4) + self.assertEqual(self.load(Reader(data)), obj) + def test_highest_protocol(self): # Of course this needs to be changed when HIGHEST_PROTOCOL changes. self.assertEqual(pickle.HIGHEST_PROTOCOL, 5) diff --git a/Misc/NEWS.d/next/Library/2026-10-05-14-30-00.gh-issue-158841.pK3vNd.rst b/Misc/NEWS.d/next/Library/2026-10-05-14-30-00.gh-issue-158841.pK3vNd.rst new file mode 100644 index 00000000000000..eda6d7daefa48d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-05-14-30-00.gh-issue-158841.pK3vNd.rst @@ -0,0 +1,3 @@ +Fix a crash in :func:`pickle.load` and :class:`pickle.Unpickler` when the +``read()`` method of the file object returns an instance of a :class:`bytes` +subclass and the requested size is large. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 5339337741d2f2..f214f71425b492 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -1439,6 +1439,17 @@ _Unpickler_ReadFromFile(PickleState *state, UnpicklerObject *self, Py_ssize_t n) if (data == NULL) { return -1; } + if (cursize < n && PyBytes_Check(data) && !PyBytes_CheckExact(data)) { + /* read() may return a bytes subclass, which cannot be resized + in place. Copy it into an exact bytes object. */ + PyObject *exact = PyBytes_FromStringAndSize( + PyBytes_AS_STRING(data), PyBytes_GET_SIZE(data)); + Py_DECREF(data); + if (exact == NULL) { + return -1; + } + data = exact; + } while (cursize < n) { Py_ssize_t prevsize = cursize; // geometrically double the chunk size to avoid CPU DoS