8000 gh-95782: Fix `io.BufferedReader.tell` etc. returning offsets < 0 · python/cpython@b5931f2 · GitHub
[go: up one dir, main page]

Skip to content

Commit b5931f2

Browse files
committed
gh-95782: Fix io.BufferedReader.tell etc. returning offsets < 0
lseek() always returns 0 for character pseudo-devices like `/dev/urandom` (for other non-regular files, e.g. `/dev/stdin`, it always returns -1, to which CPython reacts by raising appropriate exceptions). They are thus technically seekable despite not having seek semantics. When calling read() on e.g. an instance of `io.BufferedReader` that wraps such a file, `BufferedReader` reads ahead, filling its buffer, creating a discrepancy between the number of bytes read and the internal `tell()` always returning 0, which previously resulted in e.g. `BufferedReader.tell()` or `BufferedReader.seek()` being able to return positions < 0 even though these are supposed to be always >= 0. Invariably keep the return value non-negative by returning max(former_return_value, 0) instead, and add some corresponding tests.
1 parent 22d91c1 commit b5931f2

File tree

4 files changed

+62
-3
lines changed

4 files changed

+62
-3
lines changed

Lib/_pyio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,8 @@ def _readinto(self, buf, read1):
12121212
return written
12131213

12141214
def tell(self):
1215-
return _BufferedIOMixin.tell(self) - len(self._read_buf) + self._read_pos
1215+
# GH-95782: Keep return value non-negative
1216+
return max(_BufferedIOMixin.tell(self) - len(self._read_buf) + self._read_pos, 0)
12161217

12171218
def seek(self, pos, whence=0):
12181219
if whence not in valid_seek_flags:

Lib/test/test_io.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,27 @@ class PyMockUnseekableIO(MockUnseekableIO, pyio.BytesIO):
259259
UnsupportedOperation = pyio.UnsupportedOperation
260260

261261

262+
class MockCharPseudoDevFileIO(MockFileIO):
263+
# GH-95782
264+
# ftruncate() does not work on these special files (and CPython then raises
265+
# appropriate exceptions), so truncate() does not have to be accounted for
266+
# here.
267+
def __init__(self, data):
268+
super().__init__(data)
269+
270+
def seek(self, *args):
271+
return 0
272+
273+
def tell(self, *args):
274+
return 0
275+
276+
class CMockCharPseudoDevFileIO(MockCharPseudoDevFileIO, io.BytesIO):
277+
pass
278+
279+
class PyMockCharPseudoDevFileIO(MockCharPseudoDevFileIO, pyio.BytesIO):
280+
pass
281+
282+
262283
class MockNonBlockWriterIO:
263284

264285
def __init__(self):
@@ -1558,6 +1579,30 @@ def test_truncate_on_read_only(self):
15581579
self.assertRaises(self.UnsupportedOperation, bufio.truncate)
15591580
self.assertRaises(self.UnsupportedOperation, bufio.truncate, 0)
15601581

1582+
def test_tell_character_device_file(self):
1583+
# GH-95782
1584+
# For the (former) bug in BufferedIO to manifest, the wrapped IO obj
1585+
# must be able to produce at least 2 bytes.
1586+
raw = self.MockCharPseudoDevFileIO(b"12")
1587+
buf = self.tp(raw)
1588+
self.assertEqual(buf.tell(), 0)
1589+
self.assertEqual(buf.read(1), b"1")
1590+
self.assertEqual(buf.tell(), 0)
1591+
1592+
def test_seek_character_device_file(self):
1593+
raw = self.MockCharPseudoDevFileIO(b"12")
1594+
buf = self.tp(raw)
1595+
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)
1596+
self.assertEqual(buf.seek(1, io.SEEK_SET), 0)
1597+
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)
1598+
self.assertEqual(buf.read(1), b"1")
1599+
1600+
# In the C implementation, tell() sets the BufferedIO's abs_pos to 0,
1601+
# which means that the next seek() could return a negative offset if it
1602+
# does not sanity-check:
1603+
self.assertEqual(buf.tell(), 0)
1604+
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)
1605+
15611606

15621607
class CBufferedReaderTest(BufferedReaderTest, SizeofTest):
15631608
tp = io.BufferedReader
@@ -4662,7 +4707,7 @@ def load_tests(loader, tests, pattern):
46624707
# classes in the __dict__ of each test.
46634708
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
46644709
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead,
4665-
SlowFlushRawIO)
4710+
SlowFlushRawIO, MockCharPseudoDevFileIO)
46664711
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
46674712
c_io_ns = {name : getattr(io, name) for name in all_members}
46684713
py_io_ns = {name : getattr(pyio, name) for name in all_members}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`io.BufferedReader.tell`, :func:`io.BufferedReader.seek`,
2+
:func:`_pyio.BufferedReader.tell`, :func:`io.BufferedRandom.tell`,
3+
:func:`io.BufferedRandom.seek` and :func:`_pyio.BufferedRandom.tell`
4+
being able to return negative offsets.

Modules/_io/bufferedio.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,11 @@ buffered_tell(buffered *self, PyObject *Py_UNUSED(ignored))
11841184
if (pos == -1)
11851185
return NULL;
11861186
pos -= RAW_OFFSET(self);
1187-
/* TODO: sanity check (pos >= 0) */
1187+
1188+
// GH-95782
1189+
if (pos < 0)
1190+
pos = 0;
1191+
11881192
return PyLong_FromOff_t(pos);
11891193
}
11901194

@@ -1251,6 +1255,11 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
12511255
offset = target;
12521256
if (offset >= -self->pos && offset <= avail) {
12531257
self->pos += offset;
1258+
1259+
// GH-95782
1260+
if (current - avail + offset < 0)
1261+
return PyLong_FromOff_t(0);
1262+
12541263
return PyLong_FromOff_t(current - avail + offset);
12551264
}
12561265
}

0 commit comments

Comments
 (0)
0