Skip to content

Commit 979a884

Browse files
committed
fix(io): keep TextIOWrapper buffer alive during calls
1 parent d9565e5 commit 979a884

3 files changed

Lines changed: 59 additions & 12 deletions

File tree

Lib/test/test_io/test_textio.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,33 @@ def make_text(buffer):
16331633
wrapper.write('x')
16341634
self.assertRaisesRegex(ValueError, "detached", wrapper.read)
16351635

1636+
def test_reentrant_detach_during_read(self):
1637+
# gh-157363, gh-157364: The buffer must stay alive until its active
1638+
# read operation returns.
1639+
wrapper = None
1640+
1641+
class DetachOnRead(self.RawIOBase):
1642+
detached = False
1643+
1644+
def readable(self):
1645+
return True
1646+
1647+
def readinto(self, b):
1648+
if self.detached:
1649+
return 0
1650+
self.detached = True
1651+
wrapper.detach()
1652+
b[:3] = b"ab\n"
1653+
return 3
1654+
1655+
for method_name in ("read", "readline"):
1656+
with self.subTest(method_name):
1657+
raw = DetachOnRead()
1658+
wrapper = self.TextIOWrapper(
1659+
self.BufferedReader(raw), encoding="utf-8")
1660+
method = getattr(wrapper, method_name)
1661+
self.assertEqual(method(), "ab\n")
1662+
16361663
def test_reentrant_seek_during_tell(self):
16371664
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
16381665
# snapshot, so tell() re-decodes and calls the decoder's getstate(); a
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a use-after-free in :class:`io.TextIOWrapper` when a call to the
2+
underlying buffer reentrantly detaches it. Patched by Shamil Abdulaev.

Modules/_io/textio.c

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -753,43 +753,55 @@ buffer_access_safe(textio *self)
753753
return NULL;
754754
}
755755

756-
/* Returning a borrowed reference is safe since TextIOWrapper methods are
757-
protected by critical sections. */
756+
/* The critical section protects this borrowed reference until the caller
757+
can acquire its own reference. */
758758
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
759759
return self->buffer;
760760
}
761761

762+
static PyObject *
763+
buffer_acquire_safe(textio *self)
764+
{
765+
return Py_XNewRef(buffer_access_safe(self));
766+
}
767+
762768
static PyObject *
763769
buffer_getattr(textio *self, PyObject *attr_name)
764770
{
765-
PyObject *buffer = buffer_access_safe(self);
771+
PyObject *buffer = buffer_acquire_safe(self);
766772
if (buffer == NULL) {
767773
return NULL;
768774
}
769775

770-
return PyObject_GetAttr(buffer, attr_name);
776+
PyObject *res = PyObject_GetAttr(buffer, attr_name);
777+
Py_DECREF(buffer);
778+
return res;
771779
}
772780

773781
static PyObject *
774782
buffer_callmethod_noargs(textio *self, PyObject *name)
775783
{
776-
PyObject *buffer = buffer_access_safe(self);
784+
PyObject *buffer = buffer_acquire_safe(self);
777785
if (buffer == NULL) {
778786
return NULL;
779787
}
780788

781-
return PyObject_CallMethodNoArgs(buffer, name);
789+
PyObject *res = PyObject_CallMethodNoArgs(buffer, name);
790+
Py_DECREF(buffer);
791+
return res;
782792
}
783793

784794
static PyObject *
785795
buffer_callmethod_onearg(textio *self, PyObject *name, PyObject *arg)
786796
{
787-
PyObject *buffer = buffer_access_safe(self);
797+
PyObject *buffer = buffer_acquire_safe(self);
788798
if (buffer == NULL) {
789799
return NULL;
790800
}
791801

792-
return PyObject_CallMethodOneArg(buffer, name, arg);
802+
PyObject *res = PyObject_CallMethodOneArg(buffer, name, arg);
803+
Py_DECREF(buffer);
804+
return res;
793805
}
794806

795807
static void
@@ -1862,8 +1874,13 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
18621874
}
18631875

18641876
if (needflush) {
1865-
PyObject *buffer = buffer_access_safe(self);
1866-
if (buffer == NULL || _PyFile_Flush(buffer) < 0) {
1877+
PyObject *buffer = buffer_acquire_safe(self);
1878+
if (buffer == NULL) {
1879+
return NULL;
1880+
}
1881+
int res = _PyFile_Flush(buffer);
1882+
Py_DECREF(buffer);
1883+
if (res < 0) {
18671884
return NULL;
18681885
}
18691886
}
@@ -2677,11 +2694,12 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
26772694
Py_DECREF(res);
26782695
}
26792696

2680-
PyObject *buf = buffer_access_safe(self);
2697+
PyObject *buf = buffer_acquire_safe(self);
26812698
if (buf == NULL) {
26822699
goto fail;
26832700
}
26842701
res = _PyObject_CallMethod(buf, &_Py_ID(seek), "ii", 0, 2);
2702+
Py_DECREF(buf);
26852703
Py_CLEAR(cookieObj);
26862704
if (res == NULL)
26872705
goto fail;
@@ -3439,7 +3457,7 @@ static PyObject *
34393457
_io_TextIOWrapper_buffer_get_impl(textio *self)
34403458
/*[clinic end generated code: output=d265a34555aa5d4b input=5951cfa148f7350a]*/
34413459
{
3442-
return Py_XNewRef(buffer_access_safe(self));
3460+
return buffer_acquire_safe(self);
34433461
}
34443462

34453463
static PyMethodDef incrementalnewlinedecoder_methods[] = {

0 commit comments

Comments
 (0)