https://github.com/python/cpython/commit/679b3767aad75498f921c6cc3f4c3f40659f4066 commit: 679b3767aad75498f921c6cc3f4c3f40659f4066 branch: 3.14 author: Cody Maloney <[email protected]> committer: cmaloney <[email protected]> date: 2026-11-09T15:11:11-08:00 summary:
[3.14] gh-160811: fix TextIOWrapper.tell() after seek() reading past end of buffer (GH-160812) (#161703) 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.py M Modules/_io/textio.c diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 01fd821fb0049c..194ce599db1bd4 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -2417,9 +2417,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.py b/Lib/test/test_io.py index 14466086fff348..a05c10391b626b 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -3373,6 +3373,23 @@ 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") f.write("\u00e6\u0300") @@ -4412,6 +4429,17 @@ class PyTextIOWrapperTest(TextIOWrapperTest): io = pyio 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(unittest.TestCase): 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 3aeaad0cf78979..5e6a15458eda89 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -2967,9 +2967,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]
