Skip to content

Commit b19f7ec

Browse files
bpo-39681: Fix C pickle regression with minimal file-like objects (GH-18592) (#18630)
Fix a regression where the C pickle module wouldn't allow unpickling from a file-like object that doesn't expose a readinto() method. (cherry picked from commit 9f37872) Co-authored-by: Antoine Pitrou <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]>
1 parent 13951c7 commit b19f7ec

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

Lib/test/pickletester.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ def tell(self):
7373
raise io.UnsupportedOperation
7474

7575

76+
class MinimalIO(object):
77+
"""
78+
A file-like object that doesn't support readinto().
79+
"""
80+
def __init__(self, *args):
81+
self._bio = io.BytesIO(*args)
82+
self.getvalue = self._bio.getvalue
83+
self.read = self._bio.read
84+
self.readline = self._bio.readline
85+
self.write = self._bio.write
86+
87+
7688
# We can't very well test the extension registry without putting known stuff
7789
# in it, but we have to be careful to restore its original state. Code
7890
# should do this:
@@ -3361,7 +3373,7 @@ def test_reusing_unpickler_objects(self):
33613373
f.seek(0)
33623374
self.assertEqual(unpickler.load(), data2)
33633375

3364-
def _check_multiple_unpicklings(self, ioclass):
3376+
def _check_multiple_unpicklings(self, ioclass, *, seekable=True):
33653377
for proto in protocols:
33663378
with self.subTest(proto=proto):
33673379
data1 = [(x, str(x)) for x in range(2000)] + [b"abcde", len]
@@ -3374,18 +3386,23 @@ def _check_multiple_unpicklings(self, ioclass):
33743386
f = ioclass(pickled * N)
33753387
unpickler = self.unpickler_class(f)
33763388
for i in range(N):
3377-
if f.seekable():
3389+
if seekable:
33783390
pos = f.tell()
33793391
self.assertEqual(unpickler.load(), data1)
3380-
if f.seekable():
3392+
if seekable:
33813393
self.assertEqual(f.tell(), pos + len(pickled))
33823394
self.assertRaises(EOFError, unpickler.load)
33833395

33843396
def test_multiple_unpicklings_seekable(self):
33853397
self._check_multiple_unpicklings(io.BytesIO)
33863398

33873399
def test_multiple_unpicklings_unseekable(self):
3388-
self._check_multiple_unpicklings(UnseekableIO)
3400+
self._check_multiple_unpicklings(UnseekableIO, seekable=False)
3401+
3402+
def test_multiple_unpicklings_minimal(self):
3403+
# File-like object that doesn't support peek() and readinto()
3404+
# (bpo-39681)
3405+
self._check_multiple_unpicklings(MinimalIO, seekable=False)
33893406

33903407
def test_unpickling_buffering_readline(self):
33913408
# Issue #12687: the unpickler's buffering logic could fail with
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a regression where the C pickle module wouldn't allow unpickling from a
2+
file-like object that doesn't expose a readinto() method.

Modules/_pickle.c

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,13 +1371,42 @@ _Unpickler_ReadInto(UnpicklerObject *self, char *buf, Py_ssize_t n)
13711371
}
13721372

13731373
/* Read from file */
1374-
if (!self->readinto) {
1374+
if (!self->read) {
1375+
/* We're unpickling memory, this means the input is truncated */
13751376
return bad_readline();
13761377
}
13771378
if (_Unpickler_SkipConsumed(self) < 0) {
13781379
return -1;
13791380
}
13801381

1382+
if (!self->readinto) {
1383+
/* readinto() not supported on file-like object, fall back to read()
1384+
* and copy into destination buffer (bpo-39681) */
1385+
PyObject* len = PyLong_FromSsize_t(n);
1386+
if (len == NULL) {
1387+
return -1;
1388+
}
1389+
PyObject* data = _Pickle_FastCall(self->read, len);
1390+
if (data == NULL) {
1391+
return -1;
1392+
}
1393+
if (!PyBytes_Check(data)) {
1394+
PyErr_Format(PyExc_ValueError,
1395+
"read() returned non-bytes object (%R)",
1396+
Py_TYPE(data));
1397+
Py_DECREF(data);
1398+
return -1;
1399+
}
1400+
Py_ssize_t read_size = PyBytes_GET_SIZE(data);
1401+
if (read_size < n) {
1402+
Py_DECREF(data);
1403+
return bad_readline();
1404+
}
1405+
memcpy(buf, PyBytes_AS_STRING(data), n);
1406+
Py_DECREF(data);
1407+
return n;
1408+
}
1409+
13811410
/* Call readinto() into user buffer */
13821411
PyObject *buf_obj = PyMemoryView_FromMemory(buf, n, PyBUF_WRITE);
13831412
if (buf_obj == NULL) {
@@ -1606,17 +1635,19 @@ _Unpickler_SetInputStream(UnpicklerObject *self, PyObject *file)
16061635
_Py_IDENTIFIER(readinto);
16071636
_Py_IDENTIFIER(readline);
16081637

1638+
/* Optional file methods */
16091639
if (_PyObject_LookupAttrId(file, &PyId_peek, &self->peek) < 0) {
16101640
return -1;
16111641
}
1642+
if (_PyObject_LookupAttrId(file, &PyId_readinto, &self->readinto) < 0) {
1643+
return -1;
1644+
}
16121645
(void)_PyObject_LookupAttrId(file, &PyId_read, &self->read);
1613-
(void)_PyObject_LookupAttrId(file, &PyId_readinto, &self->readinto);
16141646
(void)_PyObject_LookupAttrId(file, &PyId_readline, &self->readline);
1615-
if (!self->readline || !self->readinto || !self->read) {
1647+
if (!self->readline || !self->read) {
16161648
if (!PyErr_Occurred()) {
16171649
PyErr_SetString(PyExc_TypeError,
1618-
"file must have 'read', 'readinto' and "
1619-
"'readline' attributes");
1650+
"file must have 'read' and 'readline' attributes");
16201651
}
16211652
Py_CLEAR(self->read);
16221653
Py_CLEAR(self->readinto);

0 commit comments

Comments
 (0)