diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 6e9e96b0e55262..30c34e818b1572 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -962,6 +962,27 @@ def test_args_error(self): with self.assertRaisesRegex(TypeError, "BufferedWriter"): self.tp(self.BytesIO(), 1024, 1024, 1024) + def test_non_boolean_closed_attr(self): + # gh-140650: check TypeError is raised + class MockRawIOWithoutClosed(self.MockRawIO): + closed = NotImplemented + + bufio = self.tp(MockRawIOWithoutClosed()) + self.assertRaises(TypeError, bufio.write, b"") + self.assertRaises(TypeError, bufio.flush) + self.assertRaises(TypeError, bufio.close) + + def test_closed_attr_raises(self): + class MockRawIOClosedRaises(self.MockRawIO): + @property + def closed(self): + raise ValueError("test") + + bufio = self.tp(MockRawIOClosedRaises()) + self.assertRaisesRegex(ValueError, "test", bufio.write, b"") + self.assertRaisesRegex(ValueError, "test", bufio.flush) + self.assertRaisesRegex(ValueError, "test", bufio.close) + class PyBufferedWriterTest(BufferedWriterTest, PyTestCase): tp = pyio.BufferedWriter diff --git a/Misc/NEWS.d/next/Library/2025-10-27-00-40-49.gh-issue-140650.DYJPJ9.rst b/Misc/NEWS.d/next/Library/2025-10-27-00-40-49.gh-issue-140650.DYJPJ9.rst new file mode 100644 index 00000000000000..2ae153a64808e8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-27-00-40-49.gh-issue-140650.DYJPJ9.rst @@ -0,0 +1,3 @@ +Fix an issue where closing :class:`io.BufferedWriter` could crash if +the closed attribute raised an exception on access or could not be +converted to a boolean. diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 0a2b35025321cf..0b4bc4c6b8ad42 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -362,16 +362,24 @@ _enter_buffered_busy(buffered *self) } #define IS_CLOSED(self) \ - (!self->buffer || \ + (!self->buffer ? 1 : \ (self->fast_closed_checks \ ? _PyFileIO_closed(self->raw) \ : buffered_closed(self))) #define CHECK_CLOSED(self, error_msg) \ - if (IS_CLOSED(self) && (Py_SAFE_DOWNCAST(READAHEAD(self), Py_off_t, Py_ssize_t) == 0)) { \ - PyErr_SetString(PyExc_ValueError, error_msg); \ - return NULL; \ - } \ + do { \ + int _closed = IS_CLOSED(self); \ + if (_closed < 0) { \ + return NULL; \ + } \ + if (_closed && \ + (Py_SAFE_DOWNCAST(READAHEAD(self), Py_off_t, Py_ssize_t) == 0)) \ + { \ + PyErr_SetString(PyExc_ValueError, error_msg); \ + return NULL; \ + } \ + } while (0); #define VALID_READ_BUFFER(self) \ (self->readable && self->read_end != -1) @@ -2079,6 +2087,7 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer) PyObject *res = NULL; Py_ssize_t written, avail, remaining; Py_off_t offset; + int r; CHECK_INITIALIZED(self) @@ -2087,7 +2096,11 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer) /* Issue #31976: Check for closed file after acquiring the lock. Another thread could be holding the lock while closing the file. */ - if (IS_CLOSED(self)) { + r = IS_CLOSED(self); + if (r < 0) { + goto error; + } + if (r > 0) { PyErr_SetString(PyExc_ValueError, "write to closed file"); goto error; }