From 240931999cd24ef56e087b77d127c697e4d81120 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sat, 3 Jan 2026 19:58:40 -0800 Subject: [PATCH 01/12] gh-143378: Fix use-after-free BytesIO write via re-entrant buffer access 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. Temporarily bump the exports counter while acquiring the input buffer to block re-entrant mutation, and add regression tests to ensure such cases raise BufferError instead of crashing. --- Lib/test/test_io/test_memoryio.py | 31 +++++++++++++++++++ ...-01-03-19-41-36.gh-issue-143378.29AvE7.rst | 2 ++ Modules/_io/bytesio.c | 5 +++ 3 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index bb023735e21398..4de503ea6cfbbd 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -856,6 +856,37 @@ def test_cow_mutable(self): memio = self.ioclass(ba) self.assertEqual(sys.getrefcount(ba), old_rc) + @support.cpython_only + def test_uaf_buffer_write(self): + # Prevent use-after-free when write() triggers a re-entrant call that + # closes or mutates the BytesIO object. + # See: https://github.com/python/cpython/issues/143378 + class TBuf: + def __init__(self, bio): + self.bio = bio + def __buffer__(self, flags): + self.bio.close() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(BufferError, memio.write, TBuf(memio)) + + @support.cpython_only + def test_uaf_buffer_writelines(self): + # Prevent use-after-free when writelines() triggers a re-entrant call that + # closes or mutates the BytesIO object. + # See: https://github.com/python/cpython/issues/143378 + class TBuf: + def __init__(self, bio): + self.bio = bio + def __buffer__(self, flags): + self.bio.close() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(BufferError, memio.writelines, [TBuf(memio)]) + + class CStringIOTest(PyStringIOTest): ioclass = io.StringIO UnsupportedOperation = io.UnsupportedOperation 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..01354686896616 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst @@ -0,0 +1,2 @@ +Fix use-after-free crashes when a :class:`_io.BytesIO` object is mutated during +buffer writes via :meth:`write` or :meth:`writelines`. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 96611823ab6b45..b897313a34f71e 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -202,9 +202,14 @@ write_bytes_lock_held(bytesio *self, PyObject *b) } Py_buffer buf; + /* Issue #143378: Prevent re-entrant mutation during PyObject_GetBuffer() */ + self->exports++; if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) { + self->exports--; return -1; } + self->exports--; + Py_ssize_t len = buf.len; if (len == 0) { goto done; From 2c3035cd564ba0e95e6aa91fe22f99278a9ff0e1 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sun, 4 Jan 2026 09:39:52 -0800 Subject: [PATCH 02/12] gh-143378: Fix Sphinx references in NEWS entry --- .../Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 01354686896616..386d6ccafca943 100644 --- 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 @@ -1,2 +1,2 @@ -Fix use-after-free crashes when a :class:`_io.BytesIO` object is mutated during -buffer writes via :meth:`write` or :meth:`writelines`. +Fix use-after-free crashes when a :class:`io.BytesIO` object is mutated during +buffer writes via ``write()`` or ``writelines()``. From 1a667a42dc911b10d0c5dfa30b28511338d85107 Mon Sep 17 00:00:00 2001 From: zhong <60600792+superboy-zjc@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:00:13 -0800 Subject: [PATCH 03/12] Update Lib/test/test_io/test_memoryio.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_io/test_memoryio.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 4de503ea6cfbbd..0dd889069ca994 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -872,10 +872,9 @@ def __buffer__(self, flags): self.assertRaises(BufferError, memio.write, TBuf(memio)) @support.cpython_only - def test_uaf_buffer_writelines(self): - # Prevent use-after-free when writelines() triggers a re-entrant call that - # closes or mutates the BytesIO object. - # See: https://github.com/python/cpython/issues/143378 + def test_writelines_concurrent_mutation(self): + # Prevent crashes when buf.writelines() concurrently mutates 'buf'. + # See: https://github.com/python/cpython/issues/143378. class TBuf: def __init__(self, bio): self.bio = bio From 35f98563265618ab5dfe3ce9e0e99374bb6ac22f Mon Sep 17 00:00:00 2001 From: zhong <60600792+superboy-zjc@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:00:44 -0800 Subject: [PATCH 04/12] Update Modules/_io/bytesio.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Modules/_io/bytesio.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index b897313a34f71e..8fafdab9efdffa 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -202,7 +202,6 @@ write_bytes_lock_held(bytesio *self, PyObject *b) } Py_buffer buf; - /* Issue #143378: Prevent re-entrant mutation during PyObject_GetBuffer() */ self->exports++; if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) { self->exports--; From 5997e9007a3eef30c772d2dcf13a09a4073d6249 Mon Sep 17 00:00:00 2001 From: zhong <60600792+superboy-zjc@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:01:17 -0800 Subject: [PATCH 05/12] Update Lib/test/test_io/test_memoryio.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_io/test_memoryio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 0dd889069ca994..d33121235adbaa 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -857,10 +857,10 @@ def test_cow_mutable(self): self.assertEqual(sys.getrefcount(ba), old_rc) @support.cpython_only - def test_uaf_buffer_write(self): - # Prevent use-after-free when write() triggers a re-entrant call that - # closes or mutates the BytesIO object. - # See: https://github.com/python/cpython/issues/143378 + def test_write_concurrent_mutation(self): + # Prevent crashes when buf.write() concurrently mutates 'buf'. + # See: https://github.com/python/cpython/issues/143378. + class TBuf: def __init__(self, bio): self.bio = bio From 27c99dabc535b0585e7946401ba5c09314b1c642 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sun, 4 Jan 2026 11:07:21 -0800 Subject: [PATCH 06/12] gh-143378: reduce duplication in unit tests per review --- Lib/test/test_io/test_memoryio.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index d33121235adbaa..5919cbcb2c05f6 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -861,29 +861,25 @@ def test_write_concurrent_mutation(self): # Prevent crashes when buf.write() concurrently mutates 'buf'. # See: https://github.com/python/cpython/issues/143378. - class TBuf: - def __init__(self, bio): - self.bio = bio + class B: def __buffer__(self, flags): - self.bio.close() + memio.close() return memoryview(b"A") memio = self.ioclass() - self.assertRaises(BufferError, memio.write, TBuf(memio)) + self.assertRaises(BufferError, memio.write, B()) @support.cpython_only def test_writelines_concurrent_mutation(self): # Prevent crashes when buf.writelines() concurrently mutates 'buf'. # See: https://github.com/python/cpython/issues/143378. - class TBuf: - def __init__(self, bio): - self.bio = bio + class B: def __buffer__(self, flags): - self.bio.close() + memio.close() return memoryview(b"A") memio = self.ioclass() - self.assertRaises(BufferError, memio.writelines, [TBuf(memio)]) + self.assertRaises(BufferError, memio.writelines, [B()]) class CStringIOTest(PyStringIOTest): From 586fbdc1c773d78a23f4897e3cec7d90ce11ed50 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sun, 4 Jan 2026 11:11:28 -0800 Subject: [PATCH 07/12] gh-143378: address review comments in NEWS entry --- .../Library/2026-01-03-19-41-36.gh-issue-143378.29AvE7.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 386d6ccafca943..57bbb4d0a1399c 100644 --- 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 @@ -1,2 +1 @@ -Fix use-after-free crashes when a :class:`io.BytesIO` object is mutated during -buffer writes via ``write()`` or ``writelines()``. +Fix use-after-free crashes when a :class:`~io.BytesIO` object is concurrently mutated during :meth:`~io.RawIOBase.write` or :meth:`~io.IOBase.writelines`. From 89b2cb76c700e1ceb2871e450456aa995a82a339 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sun, 4 Jan 2026 11:14:35 -0800 Subject: [PATCH 08/12] gh-143378: clean up unit tests --- Lib/test/test_io/test_memoryio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 5919cbcb2c05f6..d6c8cdff5d3590 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -860,7 +860,6 @@ def test_cow_mutable(self): def test_write_concurrent_mutation(self): # Prevent crashes when buf.write() concurrently mutates 'buf'. # See: https://github.com/python/cpython/issues/143378. - class B: def __buffer__(self, flags): memio.close() From 7805fa1411f27a1d7915130bcf83eb1fb3b35b62 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sun, 4 Jan 2026 16:18:02 -0800 Subject: [PATCH 09/12] gh-143378: refactor the buffer check and unit tests for C and Python implementation --- Lib/_pyio.py | 2 + Lib/test/test_io/test_memoryio.py | 71 ++++++++++++++++++++----------- Modules/_io/bytesio.c | 11 +++-- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 69a088df8fc987..abf85dad088f6b 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -955,6 +955,8 @@ def write(self, b): 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 d6c8cdff5d3590..f31b071df44cdf 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -587,6 +587,52 @@ def test_issue5449(self): self.ioclass(initial_bytes=buf) self.assertRaises(TypeError, self.ioclass, buf, foo=None) + def test_write_concurrent_close(self): + # Prevent crashes when memio.write() concurrently closes 'memio'. + # See: https://github.com/python/cpython/issues/143378. + class B: + def __buffer__(self, flags): + memio.close() + return memoryview(b"A") + + memio = self.ioclass() + self.assertRaises(ValueError, memio.write, B()) + + def test_writelines_concurrent_close(self): + # Prevent crashes when memio.writelines() concurrently closes 'memio'. + # See: https://github.com/python/cpython/issues/143378. + 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): + # Prevent crashes when memio.write() concurrently exports 'memio'. + # See: https://github.com/python/cpython/issues/143378. + 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): + # Prevent crashes when memio.writelines() concurrently exports 'memio'. + # See: https://github.com/python/cpython/issues/143378. + 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: @@ -856,31 +902,6 @@ def test_cow_mutable(self): memio = self.ioclass(ba) self.assertEqual(sys.getrefcount(ba), old_rc) - @support.cpython_only - def test_write_concurrent_mutation(self): - # Prevent crashes when buf.write() concurrently mutates 'buf'. - # See: https://github.com/python/cpython/issues/143378. - class B: - def __buffer__(self, flags): - memio.close() - return memoryview(b"A") - - memio = self.ioclass() - self.assertRaises(BufferError, memio.write, B()) - - @support.cpython_only - def test_writelines_concurrent_mutation(self): - # Prevent crashes when buf.writelines() concurrently mutates 'buf'. - # See: https://github.com/python/cpython/issues/143378. - class B: - def __buffer__(self, flags): - memio.close() - return memoryview(b"A") - - memio = self.ioclass() - self.assertRaises(BufferError, memio.writelines, [B()]) - - class CStringIOTest(PyStringIOTest): ioclass = io.StringIO UnsupportedOperation = io.UnsupportedOperation diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 8fafdab9efdffa..4ce3ac80685e4a 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -202,14 +202,17 @@ write_bytes_lock_held(bytesio *self, PyObject *b) } Py_buffer buf; - self->exports++; + Py_ssize_t len; if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) { - self->exports--; return -1; } - self->exports--; - 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; } From 6ac90be01f9ce9e8bcac4485f686ef71c32770a6 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Mon, 5 Jan 2026 21:22:50 -0800 Subject: [PATCH 10/12] gh-143378: clean up test comments --- Lib/test/test_io/test_memoryio.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index f31b071df44cdf..f730e38a5d6485 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -588,8 +588,6 @@ def test_issue5449(self): self.assertRaises(TypeError, self.ioclass, buf, foo=None) def test_write_concurrent_close(self): - # Prevent crashes when memio.write() concurrently closes 'memio'. - # See: https://github.com/python/cpython/issues/143378. class B: def __buffer__(self, flags): memio.close() @@ -598,9 +596,11 @@ def __buffer__(self, flags): 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): - # Prevent crashes when memio.writelines() concurrently closes 'memio'. - # See: https://github.com/python/cpython/issues/143378. class B: def __buffer__(self, flags): memio.close() @@ -610,8 +610,6 @@ def __buffer__(self, flags): self.assertRaises(ValueError, memio.writelines, [B()]) def test_write_concurrent_export(self): - # Prevent crashes when memio.write() concurrently exports 'memio'. - # See: https://github.com/python/cpython/issues/143378. class B: buf = None def __buffer__(self, flags): @@ -622,8 +620,6 @@ def __buffer__(self, flags): self.assertRaises(BufferError, memio.write, B()) def test_writelines_concurrent_export(self): - # Prevent crashes when memio.writelines() concurrently exports 'memio'. - # See: https://github.com/python/cpython/issues/143378. class B: buf = None def __buffer__(self, flags): From 2155e48e2eb346be72b1b94960f7be3a6530c8fa Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Tue, 6 Jan 2026 14:28:39 -0800 Subject: [PATCH 11/12] gh-143378: clean up duplicated shape check --- Modules/_io/bytesio.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 4ce3ac80685e4a..d088bb0efac797 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -194,13 +194,6 @@ 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) { From 4f0b8b619001e7248f0f391195474a1eb7a1b797 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Thu, 8 Jan 2026 10:52:50 -0800 Subject: [PATCH 12/12] gh-143378: clean up duplicated shape check in Python implementation --- Lib/_pyio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index abf85dad088f6b..77c44addabf225 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -949,8 +949,6 @@ 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: