From f5c8f105dae53965ba6b3cc55c10ba09f0c8f25c Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 14 Jul 2026 00:33:39 +0800 Subject: [PATCH 1/5] Fix crash when calling `io.TextIOWrapper.seek` with a malformed coo kie --- Lib/test/test_io/test_textio.py | 10 ++++++++++ .../2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst | 1 + Modules/_io/textio.c | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index 82096ab09873955..9b099e2d66a5725 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1425,6 +1425,16 @@ def test_read_non_blocking(self): os.close(r) os.close(w) + def test_seek_reject_negative_chars_to_skip(self): + data = b"1" + raw = io.BytesIO(data) + buf = io.BufferedReader(raw) + tio = io.TextIOWrapper(buf, encoding='utf_8') + # Set 'cookie.chars_to_skip' to INT_MIN + COOKIE_VALUE = (100 << (12 * 8)) | (0x80 << (19 * 8)) + tio.read() + with self.assertRaises(OSError): + tio.seek(COOKIE_VALUE) class MemviewBytesIO(io.BytesIO): '''A BytesIO object whose read method returns memoryviews diff --git a/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst b/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst new file mode 100644 index 000000000000000..04af934b03a84be --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst @@ -0,0 +1 @@ +:class:`io.TextIOWrapper`: Fix a rare crash when calling ``seek()`` with a malformed cookie. diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 5b2a20a30c28cb2..eedbe072c1516e4 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -2781,7 +2781,8 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence) textiowrapper_set_decoded_chars(self, decoded); /* Skip chars_to_skip of the decoded characters. */ - if (PyUnicode_GetLength(self->decoded_chars) < cookie.chars_to_skip) { + /* gh-153662: Reject negative chars_to_skip*/ + if (cookie.chars_to_skip < 0 || PyUnicode_GetLength(self->decoded_chars) < cookie.chars_to_skip) { PyErr_SetString(PyExc_OSError, "can't restore logical file position"); goto fail; } From cde0ad6f9213f89bb7b060671d70718036fca7ad Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 14 Jul 2026 18:05:34 +0800 Subject: [PATCH 2/5] Fix the news entry --- .../next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst b/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst index 04af934b03a84be..b5d56795272003f 100644 --- a/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst +++ b/Misc/NEWS.d/next/Library/2026-07-13-23-47-14.gh-issue-153662.ei9ya3.rst @@ -1 +1 @@ -:class:`io.TextIOWrapper`: Fix a rare crash when calling ``seek()`` with a malformed cookie. +Fix a crash when calling :meth:`io.TextIOWrapper.seek` with a malformed cookie. From b9ec776c6dbe9662d6e4c18ee7be1cd2f7813702 Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 14 Jul 2026 19:36:25 +0800 Subject: [PATCH 3/5] Address review --- Lib/test/test_io/test_textio.py | 5 ++++- Modules/_io/textio.c | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index 9b099e2d66a5725..98082aad9227a3c 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1426,6 +1426,7 @@ def test_read_non_blocking(self): os.close(w) def test_seek_reject_negative_chars_to_skip(self): + # See https://github.com/python/cpython/issues/153662 data = b"1" raw = io.BytesIO(data) buf = io.BufferedReader(raw) @@ -1433,8 +1434,10 @@ def test_seek_reject_negative_chars_to_skip(self): # Set 'cookie.chars_to_skip' to INT_MIN COOKIE_VALUE = (100 << (12 * 8)) | (0x80 << (19 * 8)) tio.read() - with self.assertRaises(OSError): + with self.assertRaises(OSError) as err: tio.seek(COOKIE_VALUE) + self.assertEqual(str(err.exception), "can't restore logical file position") + class MemviewBytesIO(io.BytesIO): '''A BytesIO object whose read method returns memoryviews diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index eedbe072c1516e4..a6c433a247bb1a8 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -2781,7 +2781,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence) textiowrapper_set_decoded_chars(self, decoded); /* Skip chars_to_skip of the decoded characters. */ - /* gh-153662: Reject negative chars_to_skip*/ + /* gh-153662: Reject negative chars_to_skip */ if (cookie.chars_to_skip < 0 || PyUnicode_GetLength(self->decoded_chars) < cookie.chars_to_skip) { PyErr_SetString(PyExc_OSError, "can't restore logical file position"); goto fail; From bf5fba60f7b788ac561a977aa9b4b80d26f5439c Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 14 Jul 2026 21:24:30 +0800 Subject: [PATCH 4/5] Add more test --- Lib/test/test_io/test_textio.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index 98082aad9227a3c..e9fd0f4ea403c0c 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1430,13 +1430,23 @@ def test_seek_reject_negative_chars_to_skip(self): data = b"1" raw = io.BytesIO(data) buf = io.BufferedReader(raw) - tio = io.TextIOWrapper(buf, encoding='utf_8') + tio = io.TextIOWrapper(buf, encoding='utf-8') # Set 'cookie.chars_to_skip' to INT_MIN COOKIE_VALUE = (100 << (12 * 8)) | (0x80 << (19 * 8)) tio.read() - with self.assertRaises(OSError) as err: + with self.assertRaisesRegex(OSError, "can't restore logical file position"): + tio.seek(COOKIE_VALUE) + + def test_seek_reject_int_max_chars_to_skip(self): + data = b"1" + raw = io.BytesIO(data) + buf = io.BufferedReader(raw) + tio = io.TextIOWrapper(buf, encoding='utf-8') + # Set 'cookie.chars_to_skip' to INT_MAX + COOKIE_VALUE = (100 << (12 * 8)) | (0x7FFFFFFF << (16 * 8)) + tio.read() + with self.assertRaisesRegex(OSError, "can't restore logical file position"): tio.seek(COOKIE_VALUE) - self.assertEqual(str(err.exception), "can't restore logical file position") class MemviewBytesIO(io.BytesIO): From 64b052a465cd96435bb2bc9afe1128f9f7e022c8 Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 14 Jul 2026 22:02:39 +0800 Subject: [PATCH 5/5] use support.subTests --- Lib/test/test_io/test_textio.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index e9fd0f4ea403c0c..f6d83200069c27d 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1425,28 +1425,21 @@ def test_read_non_blocking(self): os.close(r) os.close(w) - def test_seek_reject_negative_chars_to_skip(self): - # See https://github.com/python/cpython/issues/153662 - data = b"1" - raw = io.BytesIO(data) - buf = io.BufferedReader(raw) - tio = io.TextIOWrapper(buf, encoding='utf-8') + @support.subTests('cookie_value', [ # Set 'cookie.chars_to_skip' to INT_MIN - COOKIE_VALUE = (100 << (12 * 8)) | (0x80 << (19 * 8)) - tio.read() - with self.assertRaisesRegex(OSError, "can't restore logical file position"): - tio.seek(COOKIE_VALUE) - - def test_seek_reject_int_max_chars_to_skip(self): + (100 << (12 * 8)) | (0x80 << (19 * 8)), + # Set 'cookie.chars_to_skip' to INT_MAX + (100 << (12 * 8)) | (0x7FFFFFFF << (16 * 8)) + ]) + def test_seek_reject_invalid_chars_to_skip(self, cookie_value): + # See https://github.com/python/cpython/issues/153662 data = b"1" raw = io.BytesIO(data) buf = io.BufferedReader(raw) tio = io.TextIOWrapper(buf, encoding='utf-8') - # Set 'cookie.chars_to_skip' to INT_MAX - COOKIE_VALUE = (100 << (12 * 8)) | (0x7FFFFFFF << (16 * 8)) tio.read() with self.assertRaisesRegex(OSError, "can't restore logical file position"): - tio.seek(COOKIE_VALUE) + tio.seek(cookie_value) class MemviewBytesIO(io.BytesIO):