8000 gh-95782: Fix `io.BufferedReader.tell` etc. being able to return offsets < 0 by 6t8k · Pull Request #99709 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-95782: Fix io.BufferedReader.tell etc. being able to return offsets < 0 #99709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Lib/_pyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,8 @@ def _readinto(self, buf, read1):
return written

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

def seek(self, pos, whence=0):
if whence not in valid_seek_flags:
Expand Down
47 changes: 46 additions & 1 deletion Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,27 @@ class PyMockUnseekableIO(MockUnseekableIO, pyio.BytesIO):
UnsupportedOperation = pyio.UnsupportedOperation


class MockCharPseudoDevFileIO(MockFileIO):
# GH-95782
# ftruncate() does not work on these special files (and CPython then raises
# appropriate exceptions), so truncate() does not have to be accounted for
# here.
def __init__(self, data):
super().__init__(data)

def seek(self, *args):
return 0

def tell(self, *args):
return 0

class CMockCharPseudoDevFileIO(MockCharPseudoDevFileIO, io.BytesIO):
pass

class PyMockCharPseudoDevFileIO(MockCharPseudoDevFileIO, pyio.BytesIO):
pass


class MockNonBlockWriterIO:

def __init__(self):
Expand Down Expand Up @@ -1648,6 +1669,30 @@ def test_truncate_on_read_only(self):
self.assertRaises(self.UnsupportedOperation, bufio.truncate)
self.assertRaises(self.UnsupportedOperation, bufio.truncate, 0)

def test_tell_character_device_file(self):
# GH-95782
# For the (former) bug in BufferedIO to manifest, the wrapped IO obj
# must be able to produce at least 2 bytes.
raw = self.MockCharPseudoDevFileIO(b"12")
buf = self.tp(raw)
self.assertEqual(buf.tell(), 0)
self.assertEqual(buf.read(1), b"1")
self.assertEqual(buf.tell(), 0)

def test_seek_character_device_file(self):
raw = self.MockCharPseudoDevFileIO(b"12")
buf = self.tp(raw)
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)
self.assertEqual(buf.seek(1, io.SEEK_SET), 0)
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)
self.assertEqual(buf.read(1), b"1")

# In the C implementation, tell() sets the BufferedIO's abs_pos to 0,
# which means that the next seek() could return a negative offset if it
# does not sanity-check:
self.assertEqual(buf.tell(), 0)
self.assertEqual(buf.seek(0, io.SEEK_CUR), 0)


class CBufferedReaderTest(BufferedReaderTest, SizeofTest):
tp = io.BufferedReader
Expand Down Expand Up @@ -4880,7 +4925,7 @@ def load_tests(loader, tests, pattern):
# classes in the __dict__ of each test.
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead,
SlowFlushRawIO)
SlowFlushRawIO, MockCharPseudoDevFileIO)
all_members = io.__all__
c_io_ns = {name : getattr(io, name) for name in all_members}
py_io_ns = {name : getattr(pyio, name) for name in all_members}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`io.BufferedReader.tell`, :func:`io.BufferedReader.seek`,
:func:`_pyio.BufferedReader.tell`, :func:`io.BufferedRandom.tell`,
:func:`io.BufferedRandom.seek` and :func:`_pyio.BufferedRandom.tell`
being able to return negative offsets.
11 changes: 10 additions & 1 deletion Modules/_io/bufferedio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1325,7 +1325,11 @@ _io__Buffered_tell_impl(buffered *self)
if (pos == -1)
return NULL;
pos -= RAW_OFFSET(self);
/* TODO: sanity check (pos >= 0) */

// GH-95782
if (pos < 0)
pos = 0;

return PyLong_FromOff_t(pos);
}

Expand Down Expand Up @@ -1395,6 +1399,11 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
offset = target;
if (offset >= -self->pos && offset <= avail) {
self->pos += offset;

// GH-95782
if (current - avail + offset < 0)
return PyLong_FromOff_t(0);

return PyLong_FromOff_t(current - avail + offset);
}
}
Expand Down
0