From: zhong <60600792+superboy-zjc@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:50:56 +0000 (-0800) Subject: gh-143378: Fix use-after-free when BytesIO is concurrently mutated during write opera... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6d54b6ac7d5744e1f59d784c8e020d632d2959a3;p=thirdparty%2FPython%2Fcpython.git gh-143378: Fix use-after-free when BytesIO is concurrently mutated during write operations (GH-143408) PyObject_GetBuffer() can execute user code (e.g. via __buffer__), which may close or otherwise mutate a BytesIO object while write() or writelines() is in progress. This could invalidate the internal buffer and lead to a use-after-free. Ensure that PyObject_GetBuffer() is called before validation checks. --- diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 69a088df8fc9..77c44addabf2 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -949,12 +949,12 @@ class BytesIO(BufferedIOBase): 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 diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index bb023735e213..f730e38a5d64 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -587,6 +587,48 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): 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 000000000000..57bbb4d0a139 --- /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 96611823ab6b..d088bb0efac7 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; }