https://github.com/python/cpython/commit/ad126c954cc2f42b31bf383ecb5f1bcadff2973d
commit: ad126c954cc2f42b31bf383ecb5f1bcadff2973d
branch: main
author: tonghuaroot (童话) <[email protected]>
committer: cmaloney <[email protected]>
date: 2026-11-09T12:01:17-08:00
summary:
gh-160811: fix TextIOWrapper.tell() after seek() reading past end of buffer
(#160812)
TextIOWrapper keeps a "bytes to character" ratio as a float so it can optimize
operations. If that got to NaN or Infinity as a value then `.tell()` could
compute `skip_bytes` larger than the snapshot buffer. In the C implementation
this read past the end of the buffer, and _pyio returned a wrong position.
files:
A Misc/NEWS.d/next/Library/2026-10-28-08-12-33.gh-issue-160811.wQ3xT7.rst
M Lib/_pyio.py
M Lib/test/test_io/test_textio.py
M Modules/_io/textio.c
diff --git a/Lib/_pyio.py b/Lib/_pyio.py
index 370b518f1e5e5a6..77239f026efbb17 100644
--- a/Lib/_pyio.py
+++ b/Lib/_pyio.py
@@ -2458,9 +2458,12 @@ def tell(self):
# be O(1) in most situations (common decoders, sensible input).
# Actually, it will be exactly 1 for fixed-size codecs (all
# 8-bit codecs, also UTF-16 and UTF-32).
- skip_bytes = int(self._b2cratio * chars_to_skip)
+ skip_estimate = self._b2cratio * chars_to_skip
+ if 0 <= skip_estimate <= len(next_input):
+ skip_bytes = int(skip_estimate)
+ else:
+ skip_bytes = 0
skip_back = 1
- assert skip_bytes <= len(next_input)
while skip_bytes > 0:
decoder.setstate((b'', dec_flags))
# Decode up to temptative start point
diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py
index 6e649240f08d5bf..246b2dc6087bd3a 100644
--- a/Lib/test/test_io/test_textio.py
+++ b/Lib/test/test_io/test_textio.py
@@ -707,6 +707,22 @@ def test_tell_after_readline_with_cr(self):
remaining = f.read()
self.assertEqual(remaining, "")
+ def test_tell_after_seek_past_cr(self):
+ # tell() must tolerate a stale bytes-per-char ratio after seek().
+ for data in (b'one\rtwo\rthree\r',
+ ('ab\r' + '\u20ac' * 15).encode()):
+ expected = data.decode().replace('\r', '\n')
+ for i in range(len(expected)):
+ with self.subTest(data=data, i=i):
+ f = self.TextIOWrapper(self.BytesIO(data),
+ encoding='utf-8')
+ f.read(i)
+ pos = f.tell()
+ f.read(1)
+ f.seek(pos)
+ self.assertEqual(f.read(1), expected[i])
+ f.seek(f.tell())
+ self.assertEqual(f.read(), expected[i + 1:])
def test_seek_with_encoder_state(self):
f = self.open(os_helper.TESTFN, "w", encoding="euc_jis_2004")
@@ -1772,6 +1788,17 @@ def search(name):
class PyTextIOWrapperTest(TextIOWrapperTest, PyTestCase):
shutdown_error = "LookupError: unknown encoding: ascii"
+ def test_tell_with_invalid_b2cratio(self):
+ for ratio in (float('nan'), float('inf'), -float('inf'), 1e100):
+ with self.subTest(ratio=ratio):
+ with self.TextIOWrapper(self.BytesIO(b'abc'),
+ encoding='utf-8') as f:
+ self.assertEqual(f.read(1), 'a')
+ f._b2cratio = ratio
+ position = f.tell()
+ f.seek(position)
+ self.assertEqual(f.read(), 'bc')
+
class IncrementalNewlineDecoderTest:
diff --git
a/Misc/NEWS.d/next/Library/2026-10-28-08-12-33.gh-issue-160811.wQ3xT7.rst
b/Misc/NEWS.d/next/Library/2026-10-28-08-12-33.gh-issue-160811.wQ3xT7.rst
new file mode 100644
index 000000000000000..220845de5845a2d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-28-08-12-33.gh-issue-160811.wQ3xT7.rst
@@ -0,0 +1,2 @@
+Fix :meth:`io.TextIOWrapper.tell` reading past the end of the buffer after
+:meth:`~io.TextIOWrapper.seek`.
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index b6f3f4f17119289..4929e41ba9ccb41 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -2982,9 +2982,16 @@ _io_TextIOWrapper_tell_impl(textio *self)
/* Fast search for an acceptable start point, close to our
current pos */
- skip_bytes = (Py_ssize_t) (self->b2cratio * chars_to_skip);
+ double skip_estimate = self->b2cratio * chars_to_skip;
+ if (skip_estimate >= 0 &&
+ skip_estimate <= PyBytes_GET_SIZE(next_input))
+ {
+ skip_bytes = (Py_ssize_t)skip_estimate;
+ }
+ else {
+ skip_bytes = 0;
+ }
skip_back = 1;
- assert(skip_bytes <= PyBytes_GET_SIZE(next_input));
input = PyBytes_AS_STRING(next_input);
while (skip_bytes > 0) {
/* Decode up to temptative start point */
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]