diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index a210b81d877dc1e..0fa6d5ee65bfede 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1633,6 +1633,33 @@ def make_text(buffer): wrapper.write('x') self.assertRaisesRegex(ValueError, "detached", wrapper.read) + def test_reentrant_detach_during_read(self): + # gh-157363, gh-157364: The buffer must stay alive until its active + # read operation returns. + wrapper = None + + class DetachOnRead(self.RawIOBase): + detached = False + + def readable(self): + return True + + def readinto(self, b): + if self.detached: + return 0 + self.detached = True + wrapper.detach() + b[:3] = b"ab\n" + return 3 + + for method_name in ("read", "readline"): + with self.subTest(method_name): + raw = DetachOnRead() + wrapper = self.TextIOWrapper( + self.BufferedReader(raw), encoding="utf-8") + method = getattr(wrapper, method_name) + self.assertEqual(method(), "ab\n") + def test_reentrant_seek_during_tell(self): # gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the # snapshot, so tell() re-decodes and calls the decoder's getstate(); a diff --git a/Misc/NEWS.d/next/Library/2026-09-12-18-03-15.gh-issue-157364.Qd7mKs.rst b/Misc/NEWS.d/next/Library/2026-09-12-18-03-15.gh-issue-157364.Qd7mKs.rst new file mode 100644 index 000000000000000..3ca44d25b579109 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-12-18-03-15.gh-issue-157364.Qd7mKs.rst @@ -0,0 +1,2 @@ +Fix a use-after-free in :class:`io.TextIOWrapper` when a call to the +underlying buffer reentrantly detaches it. Patched by Shamil Abdulaev. diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index a744a885932cdf5..0520ce674c49b0b 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -753,10 +753,8 @@ buffer_access_safe(textio *self) return NULL; } - /* Returning a borrowed reference is safe since TextIOWrapper methods are - protected by critical sections. */ _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); - return self->buffer; + return Py_NewRef(self->buffer); } static PyObject * @@ -767,7 +765,9 @@ buffer_getattr(textio *self, PyObject *attr_name) return NULL; } - return PyObject_GetAttr(buffer, attr_name); + PyObject *res = PyObject_GetAttr(buffer, attr_name); + Py_DECREF(buffer); + return res; } static PyObject * @@ -778,7 +778,9 @@ buffer_callmethod_noargs(textio *self, PyObject *name) return NULL; } - return PyObject_CallMethodNoArgs(buffer, name); + PyObject *res = PyObject_CallMethodNoArgs(buffer, name); + Py_DECREF(buffer); + return res; } static PyObject * @@ -789,7 +791,9 @@ buffer_callmethod_onearg(textio *self, PyObject *name, PyObject *arg) return NULL; } - return PyObject_CallMethodOneArg(buffer, name, arg); + PyObject *res = PyObject_CallMethodOneArg(buffer, name, arg); + Py_DECREF(buffer); + return res; } static void @@ -1639,8 +1643,8 @@ _io_TextIOWrapper_detach_impl(textio *self) if (buffer == NULL) { return NULL; } - self->buffer = NULL; self->detached = 1; + Py_CLEAR(self->buffer); return buffer; } @@ -1863,7 +1867,12 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text) if (needflush) { PyObject *buffer = buffer_access_safe(self); - if (buffer == NULL || _PyFile_Flush(buffer) < 0) { + if (buffer == NULL) { + return NULL; + } + int res = _PyFile_Flush(buffer); + Py_DECREF(buffer); + if (res < 0) { return NULL; } } @@ -2682,6 +2691,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence) goto fail; } res = _PyObject_CallMethod(buf, &_Py_ID(seek), "ii", 0, 2); + Py_DECREF(buf); Py_CLEAR(cookieObj); if (res == NULL) goto fail; @@ -3439,7 +3449,7 @@ static PyObject * _io_TextIOWrapper_buffer_get_impl(textio *self) /*[clinic end generated code: output=d265a34555aa5d4b input=5951cfa148f7350a]*/ { - return Py_XNewRef(buffer_access_safe(self)); + return buffer_access_safe(self); } static PyMethodDef incrementalnewlinedecoder_methods[] = {