From: Serhiy Storchaka Date: Mon, 12 Jan 2026 08:37:28 +0000 (+0200) Subject: [3.13] gh-143378: Fix use-after-free when BytesIO is concurrently mutated during... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dcc6117628da72f41d877418a8ae8f1723546043;p=thirdparty%2FPython%2Fcpython.git [3.13] gh-143378: Fix use-after-free when BytesIO is concurrently mutated during write operations (GH-143408) (GH-143600) 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> --- diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 48c8f770f81f..59f4d06f1f5b 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -941,12 +941,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 pos = self._pos diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index 33e070e1ffdf..91b9c87160af 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/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 b7aac7731584..e149823afd37 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; }