Skip to content

Commit e19dc47

Browse files
ashm-devvstinner
andauthored
gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#157370)
Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 2791c35 commit e19dc47

3 files changed

Lines changed: 51 additions & 9 deletions

File tree

‎Lib/test/test_io/test_textio.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,36 @@ 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+
with self.assertRaisesRegex(ValueError,
1663+
"underlying buffer has been detached"):
1664+
getattr(wrapper, 'buffer')
1665+
16361666
def test_reentrant_seek_during_tell(self):
16371667
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
16381668
# 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: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -758,10 +758,8 @@ buffer_access_safe(textio *self)
758758
return NULL;
759759
}
760760

761-
/* Returning a borrowed reference is safe since TextIOWrapper methods are
762-
protected by critical sections. */
763761
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
764-
return self->buffer;
762+
return Py_NewRef(self->buffer);
765763
}
766764

767765
static PyObject *
@@ -772,7 +770,9 @@ buffer_getattr(textio *self, PyObject *attr_name)
772770
return NULL;
773771
}
774772

775-
return PyObject_GetAttr(buffer, attr_name);
773+
PyObject *res = PyObject_GetAttr(buffer, attr_name);
774+
Py_DECREF(buffer);
775+
return res;
776776
}
777777

778778
static PyObject *
@@ -783,7 +783,9 @@ buffer_callmethod_noargs(textio *self, PyObject *name)
783783
return NULL;
784784
}
785785

786-
return PyObject_CallMethodNoArgs(buffer, name);
786+
PyObject *res = PyObject_CallMethodNoArgs(buffer, name);
787+
Py_DECREF(buffer);
788+
return res;
787789
}
788790

789791
static PyObject *
@@ -794,7 +796,9 @@ buffer_callmethod_onearg(textio *self, PyObject *name, PyObject *arg)
794796
return NULL;
795797
}
796798

797-
return PyObject_CallMethodOneArg(buffer, name, arg);
799+
PyObject *res = PyObject_CallMethodOneArg(buffer, name, arg);
800+
Py_DECREF(buffer);
801+
return res;
798802
}
799803

800804
static void
@@ -1644,7 +1648,7 @@ _io_TextIOWrapper_detach_impl(textio *self)
16441648
if (buffer == NULL) {
16451649
return NULL;
16461650
}
1647-
self->buffer = NULL;
1651+
Py_CLEAR(self->buffer);
16481652
self->detached = 1;
16491653
return buffer;
16501654
}
@@ -1868,7 +1872,12 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
18681872

18691873
if (needflush) {
18701874
PyObject *buffer = buffer_access_safe(self);
1871-
if (buffer == NULL || _PyFile_Flush(buffer) < 0) {
1875+
if (buffer == NULL) {
1876+
return NULL;
1877+
}
1878+
int res = _PyFile_Flush(buffer);
1879+
Py_DECREF(buffer);
1880+
if (res < 0) {
18721881
return NULL;
18731882
}
18741883
}
@@ -2687,6 +2696,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
26872696
goto fail;
26882697
}
26892698
res = _PyObject_CallMethod(buf, &_Py_ID(seek), "ii", 0, 2);
2699+
Py_DECREF(buf);
26902700
Py_CLEAR(cookieObj);
26912701
if (res == NULL)
26922702
goto fail;
@@ -3441,7 +3451,7 @@ static PyObject *
34413451
_io_TextIOWrapper_buffer_get_impl(textio *self)
34423452
/*[clinic end generated code: output=d265a34555aa5d4b input=5951cfa148f7350a]*/
34433453
{
3444-
return Py_XNewRef(buffer_access_safe(self));
3454+
return buffer_access_safe(self);
34453455
}
34463456

34473457
static PyMethodDef incrementalnewlinedecoder_methods[] = {

0 commit comments

Comments
 (0)