From d2e381b090ba925be20564392e6fe174aedd3199 Mon Sep 17 00:00:00 2001 From: zhong <60600792+superboy-zjc@users.noreply.github.com> Date: Fri, 9 Jan 2026 03:50:56 -0800 Subject: [PATCH] [3.13] 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. (cherry picked from commit 6d54b6ac7d5744e1f59d784c8e020d632d2959a3) Co-authored-by: zhong <60600792+superboy-zjc@users.noreply.github.com> --- Lib/_pyio.py | 4 +- Lib/test/test_memoryio.py | 42 +++++++++++++++++++ ...-01-03-19-41-36.gh-issue-143378.29AvE7.rst | 1 + Modules/_io/bytesio.c | 16 +++---- 4 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 48c8f770f81f1b..59f4d06f1f5bf2 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -941,12 +941,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 33e070e1ffdf7f..91b9c87160af56 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 b7aac773158458..e149823afd376a 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -180,18 +180,18 @@ resize_buffer(bytesio *self, size_t size) Py_NO_INLINE static Py_ssize_t write_bytes(bytesio *self, PyObject *b) { - 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; }