Skip to content

Commit f264f10

Browse files
[3.14] gh-143378: Fix use-after-free when BytesIO is concurrently mutated during write operations (GH-143408) (GH-143599)
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 a299c1b commit f264f10

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
@@ -935,12 +935,12 @@ def read1(self, size=-1):
935935
return self.read(size)
936936

937937
def write(self, b):
938-
if self.closed:
939-
raise ValueError("write to closed file")
940938
if isinstance(b, str):
941939
raise TypeError("can't write str to binary stream")
942940
with memoryview(b) as view:
943941
n = view.nbytes # Size of any bytes-like object
942+
if self.closed:
943+
raise ValueError("write to closed file")
944944
if n == 0:
945945
return 0
946946
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
@@ -194,18 +194,18 @@ write_bytes_lock_held(bytesio *self, PyObject *b)
194194
{
195195
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
196196

197-
if (check_closed(self)) {
198-
return -1;
199-
}
200-
if (check_exports(self)) {
201-
return -1;
202-
}
203-
204197
Py_buffer buf;
198+
Py_ssize_t len;
205199
if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) {
206200
return -1;
207201
}
208-
Py_ssize_t len = buf.len;
202+
203+
if (check_closed(self) || check_exports(self)) {
204+
len = -1;
205+
goto done;
206+
}
207+
208+
len = buf.len;
209209
if (len == 0) {
210210
goto done;
211211
}

0 commit comments

Comments
 (0)