diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index e83dd0d4e28d00..895b12669881a7 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -642,6 +642,28 @@ 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_concurrent_close(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()) + + 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 @@ -1002,6 +1024,28 @@ def closed(self): self.assertRaisesRegex(ValueError, "test", bufio.flush) self.assertRaisesRegex(ValueError, "test", bufio.close) + def test_concurrent_close(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()) + + 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 @@ -1493,6 +1537,28 @@ def test_args_error(self): with self.assertRaisesRegex(TypeError, "BufferedRandom"): self.tp(self.BytesIO(), 1024, 1024, 1024) + def test_concurrent_close(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()) + + 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 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 new file mode 100644 index 00000000000000..d8bbaaf2f47745 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-08-19-56-40.gh-issue-143375.QCzA_8.rst @@ -0,0 +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 or detached. diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 0fdae7b2d21004..f015ba45310dc4 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -1383,6 +1383,10 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) 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)); @@ -1390,10 +1394,6 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) return NULL; } - target = PyNumber_AsOff_t(targetobj, PyExc_ValueError); - if (target == -1 && PyErr_Occurred()) - 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