Skip to content

Commit d2e381b

Browse files
superboy-zjcserhiy-storchaka
authored andcommitted
[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 6d54b6a) Co-authored-by: zhong <60600792+superboy-zjc@users.noreply.github.com>
1 parent a4a33ff commit d2e381b

File tree

4 files changed

+53
-10
lines changed

4 files changed

+53
-10
lines changed

Lib/_pyio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,12 +941,12 @@ def read1(self, size=-1):
941941
return self.read(size)
942942

943943
def write(self, b):
944-
if self.closed:
945-
raise ValueError("write to closed file")
946944
if isinstance(b, str):
947945
raise TypeError("can't write str to binary stream")
948946
with memoryview(b) as view:
949947
n = view.nbytes # Size of any bytes-like object
948+
if self.closed:
949+
raise ValueError("write to closed file")
950950
if n == 0:
951951
return 0
952952
pos = self._pos

Lib/test/test_memoryio.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,48 @@ def test_issue5449(self):
587587
self.ioclass(initial_bytes=buf)
588588
self.assertRaises(TypeError, self.ioclass, buf, foo=None)
589589

590+
def test_write_concurrent_close(self):
591+
class B:
592+
def __buffer__(self, flags):
593+
memio.close()
594+
return memoryview(b"A")
595+
596+
memio = self.ioclass()
597+
self.assertRaises(ValueError, memio.write, B())
598+
599+
# Prevent crashes when memio.write() or memio.writelines()
600+
# concurrently mutates (e.g., closes or exports) 'memio'.
601+
# See: https://github.com/python/cpython/issues/143378.
602+
603+
def test_writelines_concurrent_close(self):
604+
class B:
605+
def __buffer__(self, flags):
606+
memio.close()
607+
return memoryview(b"A")
608+
609+
memio = self.ioclass()
610+
self.assertRaises(ValueError, memio.writelines, [B()])
611+
612+
def test_write_concurrent_export(self):
613+
class B:
614+
buf = None
615+
def __buffer__(self, flags):
616+
self.buf = memio.getbuffer()
617+
return memoryview(b"A")
618+
619+
memio = self.ioclass()
620+
self.assertRaises(BufferError, memio.write, B())
621+
622+
def test_writelines_concurrent_export(self):
623+
class B:
624+
buf = None
625+
def __buffer__(self, flags):
626+
self.buf = memio.getbuffer()
627+
return memoryview(b"A")
628+
629+
memio = self.ioclass()
630+
self.assertRaises(BufferError, memio.writelines, [B()])
631+
590632

591633
class TextIOTestMixin:
592634

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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`.

Modules/_io/bytesio.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,18 @@ resize_buffer(bytesio *self, size_t size)
180180
Py_NO_INLINE static Py_ssize_t
181181
write_bytes(bytesio *self, PyObject *b)
182182
{
183-
if (check_closed(self)) {
184-
return -1;
185-
}
186-
if (check_exports(self)) {
187-
return -1;
188-
}
189-
190183
Py_buffer buf;
184+
Py_ssize_t len;
191185
if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) {
192186
return -1;
193187
}
194-
Py_ssize_t len = buf.len;
188+
189+
if (check_closed(self) || check_exports(self)) {
190+
len = -1;
191+
goto done;
192+
}
193+
194+
len = buf.len;
195195
if (len == 0) {
196196
goto done;
197197
}

0 commit comments

Comments
 (0)