diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 612e4a175e5a65..23e81e850dad04 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -935,12 +935,12 @@ def read1(self, size=-1): return self.read(size) def write(self, b): - if self.closed: - raise ValueError("write to closed file") if isinstance(b, str): raise TypeError("can't write str to binary stream") with memoryview(b) as view: n = view.nbytes # Size of any bytes-like object + if self.closed: + raise ValueError("write to closed file") if n == 0: return 0 pos = self._pos diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index bb023735e21398..f730e38a5d6485 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/test_memoryio.py @@ -587,6 +587,48 @@ def test_issue5449(self): self.ioclass(initial_bytes=buf) self.assertRaises(TypeError, self.ioclass, buf, foo=None) + def test_write_concurrent_close(self): + class B: + def __buffer__(self, flags): + memio.close() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(ValueError, memio.write, B()) + + # Prevent crashes when memio.write() or memio.writelines() + # concurrently mutates (e.g., closes or exports) 'memio'. + # See: https://github.com/python/cpython/issues/143378. + + def test_writelines_concurrent_close(self): + class B: + def __buffer__(self, flags): + memio.close() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(ValueError, memio.writelines, [B()]) + + def test_write_concurrent_export(self): + class B: + buf = None + def __buffer__(self, flags): + self.buf = memio.getbuffer() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(BufferError, memio.write, B()) + + def test_writelines_concurrent_export(self): + class B: + buf = None + def __buffer__(self, flags): + self.buf = memio.getbuffer() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(BufferError, memio.writelines, [B()]) + class TextIOTestMixin: diff --git a/Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst b/Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst new file mode 100644 index 00000000000000..57bbb4d0a1399c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst @@ -0,0 +1 @@ +Fix use-after-free crashes when a :class:`~io.BytesIO` object is concurrently mutated during :meth:`~io.RawIOBase.write` or :meth:`~io.IOBase.writelines`. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index a3b653f30dd2cf..a7fa3db700dc15 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -194,18 +194,18 @@ write_bytes_lock_held(bytesio *self, PyObject *b) { _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); - if (check_closed(self)) { - return -1; - } - if (check_exports(self)) { - return -1; - } - Py_buffer buf; + Py_ssize_t len; if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) { return -1; } - Py_ssize_t len = buf.len; + + if (check_closed(self) || check_exports(self)) { + len = -1; + goto done; + } + + len = buf.len; if (len == 0) { goto done; }