https://github.com/python/cpython/commit/e19dc470f4c2afd7ac944896232a3a6ca0204399 commit: e19dc470f4c2afd7ac944896232a3a6ca0204399 branch: main author: Shamil <[email protected]> committer: vstinner <[email protected]> date: 2026-10-07T18:08:54Z summary:
gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#157370) Co-authored-by: Victor Stinner <[email protected]> files: A Misc/NEWS.d/next/Library/2026-09-12-18-03-15.gh-issue-157364.Qd7mKs.rst M Lib/test/test_io/test_textio.py M Modules/_io/textio.c diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index f9fb84101d4a2eb..65b0812f071e215 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1633,6 +1633,36 @@ 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") + with self.assertRaisesRegex(ValueError, + "underlying buffer has been detached"): + getattr(wrapper, 'buffer') + 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 8f98f7d80095b0b..3736179807f45dd 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -758,10 +758,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 * @@ -772,7 +770,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 * @@ -783,7 +783,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 * @@ -794,7 +796,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 @@ -1644,7 +1648,7 @@ _io_TextIOWrapper_detach_impl(textio *self) if (buffer == NULL) { return NULL; } - self->buffer = NULL; + Py_CLEAR(self->buffer); self->detached = 1; return buffer; } @@ -1868,7 +1872,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; } } @@ -2687,6 +2696,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; @@ -3441,7 +3451,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[] = { _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
