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]

Reply via email to