Skip to content

Commit 6d54b6a

Browse files
authored
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.
1 parent dcdb23f commit 6d54b6a

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

951951
def write(self, b):
952-
if self.closed:
953-
raise ValueError("write to closed file")
954952
if isinstance(b, str):
955953
raise TypeError("can't write str to binary stream")
956954
with memoryview(b) as view:
957955
n = view.nbytes # Size of any bytes-like object
956+
if self.closed:
957+
raise ValueError("write to closed file")
958958
if n == 0:
959959
return 0
960960

Lib/test/test_io/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)