https://github.com/python/cpython/commit/d8f6384aff25e4f4ed3544c7ec6903957897b292
commit: d8f6384aff25e4f4ed3544c7ec6903957897b292
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: cmaloney <[email protected]>
date: 2026-11-09T15:11:03-08:00
summary:

[3.15] gh-160811: fix TextIOWrapper.tell() after seek() reading past end of 
buffer (GH-160812) (#161702)

gh-160811: fix TextIOWrapper.tell() after seek() reading past end of buffer 
(GH-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.
(cherry picked from commit ad126c954cc2f42b31bf383ecb5f1bcadff2973d)

Co-authored-by: tonghuaroot (童话) <[email protected]>

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 b351c35e4bbea5..22f5b9ab9aeacd 100644
--- a/Lib/_pyio.py
+++ b/Lib/_pyio.py
@@ -2441,9 +2441,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 185a4ef0b4c56f..f52737f399c3de 100644
--- a/Lib/test/test_io/test_textio.py
+++ b/Lib/test/test_io/test_textio.py
@@ -706,6 +706,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")
@@ -1721,6 +1737,17 @@ def readinto(self, b):
 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 00000000000000..220845de5845a2
--- /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 6641d12848be59..5f9e517a13a5e4 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -2953,9 +2953,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]

Reply via email to