From f9afb39a7cc07490aae204824d1459030f456612 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Wed, 7 Jan 2026 23:58:04 +0100 Subject: [PATCH 01/13] Fix crash when seeking a closed BufferedWriter --- Lib/test/test_io/test_bufferedio.py | 11 +++++++++++ Modules/_io/bufferedio.c | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 3278665bdc9dd3..f77cd759ed8229 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -983,6 +983,17 @@ def closed(self): self.assertRaisesRegex(ValueError, "test", bufio.flush) self.assertRaisesRegex(ValueError, "test", bufio.close) + def test_gh_143375(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.close() + return 0 + + with self.assertRaisesRegex(ValueError, "seek of closed file"): + bufio.seek(EvilIndex()) + class PyBufferedWriterTest(BufferedWriterTest, PyTestCase): tp = pyio.BufferedWriter diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 4602f2b42a6017..65dfaafc7f43ef 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -1393,6 +1393,10 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) if (target == -1 && PyErr_Occurred()) return NULL; + // PyNumber_AsOff_t calls user code via __index__, which + // could have closed the file. + CHECK_CLOSED(self, "seek of closed file") + /* SEEK_SET and SEEK_CUR are special because we could seek inside the buffer. Other whence values must be managed without this optimization. Some Operating Systems can provide additional values, like From 039bf4151ed411c2520ff63eede8c6b102ef10c3 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 8 Jan 2026 19:56:47 +0100 Subject: [PATCH 02/13] Add news entry --- .../2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst new file mode 100644 index 00000000000000..cc710d97740228 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -0,0 +1,2 @@ +Fix a crash in :meth:`~io.BufferedWriter.seek` when passing an object with a +specially crafted :meth:`~object.__index__`. From 2ae393b05a681132bc72e5117f530eda8537c4f7 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 8 Jan 2026 21:59:31 +0100 Subject: [PATCH 03/13] Try with a :func: --- .../2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst index cc710d97740228..fa4306d230ecc5 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -1,2 +1,2 @@ -Fix a crash in :meth:`~io.BufferedWriter.seek` when passing an object with a +Fix a crash in :func:`~io.BufferedWriter.seek` when passing an object with a specially crafted :meth:`~object.__index__`. From d0c7ff5e6c62eb767bf3aa03b6ed5aaa1d896658 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 8 Jan 2026 22:00:23 +0100 Subject: [PATCH 04/13] Move news entry to Library --- .../2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/{Core_and_Builtins => Library}/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst (100%) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst similarity index 100% rename from Misc/NEWS.d/next/Core_and_Builtins/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst rename to Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst From f72095014be6984bc9ea25ae1be1b585f02d0b1c Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 8 Jan 2026 22:15:11 +0100 Subject: [PATCH 05/13] Fix news entry --- .../Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst index fa4306d230ecc5..697343c7e8fa6a 100644 --- a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst +++ b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -1,2 +1,2 @@ -Fix a crash in :func:`~io.BufferedWriter.seek` when passing an object with a -specially crafted :meth:`~object.__index__`. +Fix a crash in the ``seek`` method of :class:`~io.BufferedWriter` when +passing an object with a specially crafted :meth:`~object.__index__`. From ad20cdab484a171e2bc307e54a4fff05ab70b72b Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 15:45:12 +0200 Subject: [PATCH 06/13] Remove comment --- Modules/_io/bufferedio.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index fd49f95f991879..6047f0d6ef2a6a 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -1394,8 +1394,6 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) if (target == -1 && PyErr_Occurred()) return NULL; - // PyNumber_AsOff_t calls user code via __index__, which - // could have closed the file. CHECK_CLOSED(self, "seek of closed file") /* SEEK_SET and SEEK_CUR are special because we could seek inside the From a21ef456d4c1aa86b78760864f345177d1bc27cc Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 15:46:22 +0200 Subject: [PATCH 07/13] Remove redundant CHECK_CLOSED call --- Modules/_io/bufferedio.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 6047f0d6ef2a6a..2a254a08d7585d 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -1383,8 +1383,6 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) return NULL; } - CHECK_CLOSED(self, "seek of closed file") - _PyIO_State *state = find_io_state_by_def(Py_TYPE(self)); if (_PyIOBase_check_seekable(state, self->raw, Py_True) == NULL) { return NULL; From 92e3fdebe85167bcf47ee8a379ae16186262c6d1 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 15:48:44 +0200 Subject: [PATCH 08/13] Move PyNumber_AsOff_t after whence check --- Modules/_io/bufferedio.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 2a254a08d7585d..f015ba45310dc4 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -1383,17 +1383,17 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) return NULL; } - _PyIO_State *state = find_io_state_by_def(Py_TYPE(self)); - if (_PyIOBase_check_seekable(state, self->raw, Py_True) == NULL) { - return NULL; - } - target = PyNumber_AsOff_t(targetobj, PyExc_ValueError); if (target == -1 && PyErr_Occurred()) return NULL; CHECK_CLOSED(self, "seek of closed file") + _PyIO_State *state = find_io_state_by_def(Py_TYPE(self)); + if (_PyIOBase_check_seekable(state, self->raw, Py_True) == NULL) { + return NULL; + } + /* SEEK_SET and SEEK_CUR are special because we could seek inside the buffer. Other whence values must be managed without this optimization. Some Operating Systems can provide additional values, like From 5dbe999c83342bab7734ca43e835466521b97592 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 16:02:33 +0200 Subject: [PATCH 09/13] Add tests for BufferedReader and BufferedRandom --- Lib/test/test_io/test_bufferedio.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 43df27c6d85a09..6b4e7b7754cd7d 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -642,6 +642,16 @@ def test_read1_error_does_not_cause_reentrant_failure(self): # Used to crash before gh-143689: self.assertEqual(bufio.read1(1), b"h") + def test_gh_143375(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.close() + return 0 + + with self.assertRaisesRegex(ValueError, "seek of closed file"): + bufio.seek(EvilIndex()) class PyBufferedReaderTest(BufferedReaderTest, PyTestCase): tp = pyio.BufferedReader @@ -1504,6 +1514,17 @@ def test_args_error(self): with self.assertRaisesRegex(TypeError, "BufferedRandom"): self.tp(self.BytesIO(), 1024, 1024, 1024) + def test_gh_143375(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.close() + return 0 + + with self.assertRaisesRegex(ValueError, "seek of closed file"): + bufio.seek(EvilIndex()) + class PyBufferedRandomTest(BufferedRandomTest, PyTestCase): tp = pyio.BufferedRandom From 05e9afd1da89962b0271d03bf237dfdfa2728044 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 16:08:49 +0200 Subject: [PATCH 10/13] Add tests for concurrent detach --- Lib/test/test_io/test_bufferedio.py | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 6b4e7b7754cd7d..9e8672a0a8b77f 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -642,7 +642,7 @@ def test_read1_error_does_not_cause_reentrant_failure(self): # Used to crash before gh-143689: self.assertEqual(bufio.read1(1), b"h") - def test_gh_143375(self): + def test_concurrent_close(self): bufio = self.tp(self.MockRawIO()) class EvilIndex: @@ -653,6 +653,17 @@ def __index__(self): with self.assertRaisesRegex(ValueError, "seek of closed file"): bufio.seek(EvilIndex()) + def test_concurrent_detach(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.detach() + return 0 + + with self.assertRaisesRegex(ValueError, "raw stream has been detached"): + bufio.seek(EvilIndex()) + class PyBufferedReaderTest(BufferedReaderTest, PyTestCase): tp = pyio.BufferedReader @@ -1012,7 +1023,7 @@ def closed(self): self.assertRaisesRegex(ValueError, "test", bufio.flush) self.assertRaisesRegex(ValueError, "test", bufio.close) - def test_gh_143375(self): + def test_concurrent_close(self): bufio = self.tp(self.MockRawIO()) class EvilIndex: @@ -1023,6 +1034,17 @@ def __index__(self): with self.assertRaisesRegex(ValueError, "seek of closed file"): bufio.seek(EvilIndex()) + def test_concurrent_detach(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.detach() + return 0 + + with self.assertRaisesRegex(ValueError, "raw stream has been detached"): + bufio.seek(EvilIndex()) + class PyBufferedWriterTest(BufferedWriterTest, PyTestCase): tp = pyio.BufferedWriter @@ -1514,7 +1536,7 @@ def test_args_error(self): with self.assertRaisesRegex(TypeError, "BufferedRandom"): self.tp(self.BytesIO(), 1024, 1024, 1024) - def test_gh_143375(self): + def test_concurrent_close(self): bufio = self.tp(self.MockRawIO()) class EvilIndex: @@ -1525,6 +1547,17 @@ def __index__(self): with self.assertRaisesRegex(ValueError, "seek of closed file"): bufio.seek(EvilIndex()) + def test_concurrent_detach(self): + bufio = self.tp(self.MockRawIO()) + + class EvilIndex: + def __index__(self): + bufio.detach() + return 0 + + with self.assertRaisesRegex(ValueError, "raw stream has been detached"): + bufio.seek(EvilIndex()) + class PyBufferedRandomTest(BufferedRandomTest, PyTestCase): tp = pyio.BufferedRandom From e43c25f6d3dc5bd8d397ae829df947e6e1194bb6 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 16:11:20 +0200 Subject: [PATCH 11/13] Improve news entry --- .../Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst index 697343c7e8fa6a..831e3c7d04bd73 100644 --- a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst +++ b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -1,2 +1,3 @@ -Fix a crash in the ``seek`` method of :class:`~io.BufferedWriter` when -passing an object with a specially crafted :meth:`~object.__index__`. +Fix a crash in the ``seek`` method of :class:`~io.BufferedWriter`, +:class:`~io.BufferedReader` and :class:`~io.BufferedRandom` when the +stream is concurrently closed. From 0917ab19bb755fd72a466c8d41aa01317deb21f8 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 16:13:33 +0200 Subject: [PATCH 12/13] Add a newline --- Lib/test/test_io/test_bufferedio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 9e8672a0a8b77f..895b12669881a7 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -664,6 +664,7 @@ def __index__(self): with self.assertRaisesRegex(ValueError, "raw stream has been detached"): bufio.seek(EvilIndex()) + class PyBufferedReaderTest(BufferedReaderTest, PyTestCase): tp = pyio.BufferedReader From 276a2fbd29a506ceb746c13684c23d2c7dee903d Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 5 Apr 2026 16:49:31 +0200 Subject: [PATCH 13/13] Update news entry --- .../next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst index 831e3c7d04bd73..d8bbaaf2f47745 100644 --- a/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst +++ b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -1,3 +1,3 @@ Fix a crash in the ``seek`` method of :class:`~io.BufferedWriter`, :class:`~io.BufferedReader` and :class:`~io.BufferedRandom` when the -stream is concurrently closed. +stream is concurrently closed or detached.