https://github.com/python/cpython/commit/13fc16c9c0e2b2055aa1d01c93cff2e8b50c445c
commit: 13fc16c9c0e2b2055aa1d01c93cff2e8b50c445c
branch: 3.14
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-16T23:53:10Z
summary:

[3.14] gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant 
detach (#159600)

[3.15] gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant 
detach (#159591)

* gh-143008: Fix Null pointer dereferences in TextIOWrapper underlying stream 
access (#145957)

TextIOWrapper keeps its underlying stream in a member called
`self->buffer`. That stream can be detached by user code, such as custom
`.flush` implementations resulting in `self->buffer` being set to NULL.
The implementation often checked at the start of functions if
`self->buffer` is in a good state, but did not always recheck after
other Python code was called which could modify `self->buffer`.

The cases which need to be re-checked are hard to spot so rather than
rely on reviewer effort create better safety by making all self->buffer
access go through helper functions.

Thank you yihong0618 for the test, NEWS and initial implementation in
gh-143041.



(cherry picked from commit db4b1948bc4ad5e7230fe1f2814da409ca1d669b)

* gh-154523: Fix data-race in `TextIOWrapper.detach()` (#154565)

(cherry picked from commit 8440a6a4651f93ecd1c7768ffb2c735c5b9fb984)

* gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach 
(#157370)


(cherry picked from commit e19dc470f4c2afd7ac944896232a3a6ca0204399)

---------





(cherry picked from commit ea1312bdb446a536964ec7133625ae6aa6a592bf)

Co-authored-by: Cody Maloney <[email protected]>
Co-authored-by: yihong0618 <[email protected]>
Co-authored-by: Brij Kapadia <[email protected]>
Co-authored-by: Shamil <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-12-21-17-56-37.gh-issue-143008.aakErJ.rst
A Misc/NEWS.d/next/Library/2026-06-04-18-22-56.gh-issue-143008.z5tw-J.rst
A Misc/NEWS.d/next/Library/2026-07-23-18-53-48.gh-issue-154523.XUSRBO.rst
A Misc/NEWS.d/next/Library/2026-09-12-18-03-15.gh-issue-157364.Qd7mKs.rst
M Lib/test/test_free_threading/test_io.py
M Lib/test/test_io.py
M Modules/_io/clinic/textio.c.h
M Modules/_io/textio.c

diff --git a/Lib/test/test_free_threading/test_io.py 
b/Lib/test/test_free_threading/test_io.py
index 78ad33fe900a78b..e7f013b0052dde5 100644
--- a/Lib/test/test_free_threading/test_io.py
+++ b/Lib/test/test_free_threading/test_io.py
@@ -191,3 +191,23 @@ def reset_worker():
                 decoder.reset()
 
         run_concurrently([decode_worker] * 2 + [reset_worker] * 2)
+
+
+class TextIOWrapperTest(TestCase):
+    def test_buffer_detach_race(self):
+        make = lambda: io.TextIOWrapper(io.BytesIO())
+        slot = [make()]
+
+        def reader():
+            for _ in range(1000):
+                try:
+                    slot[0].buffer
+                except ValueError:
+                    pass
+
+        def detacher():
+            for _ in range(1000):
+                slot[0] = make()
+                slot[0].detach()
+
+        run_concurrently([reader, detacher])
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index d4b11b5bf01936a..d11e66e94b49708 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -4276,6 +4276,86 @@ def closed(self):
         wrapper = self.TextIOWrapper(raw)
         wrapper.close()  # should not crash
 
+    def test_reentrant_detach_during_flush(self):
+        # gh-143008: Reentrant detach() during flush should not crash.
+
+        class DetachOnce(self.BufferedRandom):
+            wrapper = None
+
+            def detach_once(self):
+                original = self.wrapper
+                self.wrapper = None
+                if original is not None:
+                    original.detach()
+                    original.flush()
+
+        class DetachOnFlush(DetachOnce):
+            def flush(self):
+                self.detach_once()
+
+        class DetachOnWrite(DetachOnce):
+            def write(self, b):
+                self.detach_once()
+                return len(b)
+
+        # Separate reference for after detach_once.
+        wrapper = None
+
+        def make_text(buffer):
+            nonlocal wrapper
+            buffer.wrapper = self.TextIOWrapper(buffer, encoding='utf-8')
+            wrapper = buffer.wrapper
+
+        # Many calls could result in the same null self->buffer crash.
+        tests = [
+            ('truncate', lambda: wrapper.truncate(0)),
+            ('close', lambda: wrapper.close()),
+            ('detach', lambda: wrapper.detach()),
+            ('seek', lambda: wrapper.seek(0)),
+            ('tell', lambda: wrapper.tell()),
+            ('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)),
+        ]
+        for name, method in tests:
+            with self.subTest(name):
+                make_text(DetachOnFlush(self.MockRawIO()))
+                self.assertRaisesRegex(ValueError, "detached", method)
+
+        # Should not crash.
+        with self.subTest('read via writeflush'):
+            make_text(DetachOnWrite(self.MockRawIO()))
+            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')
+
 
 class PyTextIOWrapperTest(TextIOWrapperTest):
     io = pyio
diff --git 
a/Misc/NEWS.d/next/Library/2025-12-21-17-56-37.gh-issue-143008.aakErJ.rst 
b/Misc/NEWS.d/next/Library/2025-12-21-17-56-37.gh-issue-143008.aakErJ.rst
new file mode 100644
index 000000000000000..907cdb770ab65f9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-12-21-17-56-37.gh-issue-143008.aakErJ.rst
@@ -0,0 +1,2 @@
+Fix crash in :class:`io.TextIOWrapper` when reentrant
+:meth:`io.TextIOBase.detach` is called reentrantly from the underlying buffer.
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-04-18-22-56.gh-issue-143008.z5tw-J.rst 
b/Misc/NEWS.d/next/Library/2026-06-04-18-22-56.gh-issue-143008.z5tw-J.rst
new file mode 100644
index 000000000000000..e99bc39c45f9b8f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-04-18-22-56.gh-issue-143008.z5tw-J.rst
@@ -0,0 +1 @@
+Fix race conditions when re-initializing a :class:`io.TextIOWrapper` object.
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-23-18-53-48.gh-issue-154523.XUSRBO.rst 
b/Misc/NEWS.d/next/Library/2026-07-23-18-53-48.gh-issue-154523.XUSRBO.rst
new file mode 100644
index 000000000000000..23b25a26effb642
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-23-18-53-48.gh-issue-154523.XUSRBO.rst
@@ -0,0 +1,2 @@
+Fixed data-race when calling :meth:`io.TextIOBase.detach` in
+:term:`free-threaded build`.
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/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h
index 99a3eb9f2607076..10d0f1390cccbb4 100644
--- a/Modules/_io/clinic/textio.c.h
+++ b/Modules/_io/clinic/textio.c.h
@@ -1337,4 +1337,29 @@ _io_TextIOWrapper__CHUNK_SIZE_set(PyObject *self, 
PyObject *value, void *Py_UNUS
 
     return return_value;
 }
-/*[clinic end generated code: output=cec74a7476964ae4 input=a9049054013a1b77]*/
+
+#if !defined(_io_TextIOWrapper_buffer_DOCSTR)
+#  define _io_TextIOWrapper_buffer_DOCSTR NULL
+#endif
+#if defined(_IO_TEXTIOWRAPPER_BUFFER_GETSETDEF)
+#  undef _IO_TEXTIOWRAPPER_BUFFER_GETSETDEF
+#  define _IO_TEXTIOWRAPPER_BUFFER_GETSETDEF {"buffer", 
(getter)_io_TextIOWrapper_buffer_get, (setter)_io_TextIOWrapper_buffer_set, 
_io_TextIOWrapper_buffer_DOCSTR},
+#else
+#  define _IO_TEXTIOWRAPPER_BUFFER_GETSETDEF {"buffer", 
(getter)_io_TextIOWrapper_buffer_get, NULL, _io_TextIOWrapper_buffer_DOCSTR},
+#endif
+
+static PyObject *
+_io_TextIOWrapper_buffer_get_impl(textio *self);
+
+static PyObject *
+_io_TextIOWrapper_buffer_get(PyObject *self, void *Py_UNUSED(context))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _io_TextIOWrapper_buffer_get_impl((textio *)self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+/*[clinic end generated code: output=e93032a0691ff0e4 input=a9049054013a1b77]*/
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index 8750c01bf177729..0125e5e5a2d2ba6 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -672,6 +672,8 @@ struct textio
     int ok; /* initialized? */
     int detached;
     Py_ssize_t chunk_size;
+    /* Use helpers buffer_*() functions to access buffer; many operations can 
set it to
+       NULL (see gh-143008, gh-142594). */
     PyObject *buffer;
     PyObject *encoding;
     PyObject *encoder;
@@ -729,6 +731,71 @@ struct textio
 
 #define textio_CAST(op) ((textio *)(op))
 
+/* Helpers to safely operate on self->buffer.
+
+   self->buffer can be detached (set to NULL) by any user code that is called
+   leading to NULL pointer dereferences (see gh-143008, gh-142594). Protect
+   against that by using helpers to check self->buffer validity at callsites. 
*/
+static PyObject *
+buffer_access_safe(textio *self)
+{
+    /* Check self->buffer directly but match errors of CHECK_ATTACHED since 
this
+       is called during construction and finalization where self->ok == 0. */
+    if (self->buffer == NULL) {
+        if (self->ok <= 0) {
+            PyErr_SetString(PyExc_ValueError,
+                            "I/O operation on uninitialized object");
+        }
+        else {
+            PyErr_SetString(PyExc_ValueError,
+                            "underlying buffer has been detached");
+        }
+        return NULL;
+    }
+
+    _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
+    return Py_NewRef(self->buffer);
+}
+
+static PyObject *
+buffer_getattr(textio *self, PyObject *attr_name)
+{
+    PyObject *buffer = buffer_access_safe(self);
+    if (buffer == NULL) {
+        return NULL;
+    }
+
+    PyObject *res = PyObject_GetAttr(buffer, attr_name);
+    Py_DECREF(buffer);
+    return res;
+}
+
+static PyObject *
+buffer_callmethod_noargs(textio *self, PyObject *name)
+{
+    PyObject *buffer = buffer_access_safe(self);
+    if (buffer == NULL) {
+        return NULL;
+    }
+
+    PyObject *res = PyObject_CallMethodNoArgs(buffer, name);
+    Py_DECREF(buffer);
+    return res;
+}
+
+static PyObject *
+buffer_callmethod_onearg(textio *self, PyObject *name, PyObject *arg)
+{
+    PyObject *buffer = buffer_access_safe(self);
+    if (buffer == NULL) {
+        return NULL;
+    }
+
+    PyObject *res = PyObject_CallMethodOneArg(buffer, name, arg);
+    Py_DECREF(buffer);
+    return res;
+}
+
 static void
 textiowrapper_set_decoded_chars(textio *self, PyObject *chars);
 
@@ -898,7 +965,7 @@ _textiowrapper_set_decoder(textio *self, PyObject 
*codec_info,
     PyObject *res;
     int r;
 
-    res = PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(readable));
+    res = buffer_callmethod_noargs(self, &_Py_ID(readable));
     if (res == NULL)
         return -1;
 
@@ -954,7 +1021,7 @@ _textiowrapper_set_encoder(textio *self, PyObject 
*codec_info,
     PyObject *res;
     int r;
 
-    res = PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(writable));
+    res = buffer_callmethod_noargs(self, &_Py_ID(writable));
     if (res == NULL)
         return -1;
 
@@ -1000,8 +1067,7 @@ _textiowrapper_fix_encoder_state(textio *self)
 
     self->encoding_start_of_stream = 1;
 
-    PyObject *cookieObj = PyObject_CallMethodNoArgs(
-        self->buffer, &_Py_ID(tell));
+    PyObject *cookieObj = buffer_callmethod_noargs(self, &_Py_ID(tell));
     if (cookieObj == NULL) {
         return -1;
     }
@@ -1569,12 +1635,15 @@ _io_TextIOWrapper_detach_impl(textio *self)
 /*[clinic end generated code: output=7ba3715cd032d5f2 input=c908a3b4ef203b0f]*/
 {
     PyObject *buffer;
-    CHECK_ATTACHED(self);
     if (_PyFile_Flush((PyObject *)self) < 0) {
         return NULL;
     }
-    buffer = self->buffer;
-    self->buffer = NULL;
+    /* _PyFile_Flush could detach before returning; raise an exception. */
+    buffer = buffer_access_safe(self);
+    if (buffer == NULL) {
+        return NULL;
+    }
+    Py_CLEAR(self->buffer);
     self->detached = 1;
     return buffer;
 }
@@ -1642,7 +1711,7 @@ _textiowrapper_writeflush(textio *self)
 
     PyObject *ret;
     do {
-        ret = PyObject_CallMethodOneArg(self->buffer, &_Py_ID(write), b);
+        ret = buffer_callmethod_onearg(self, &_Py_ID(write), b);
     } while (ret == NULL && _PyIO_trap_eintr());
     Py_DECREF(b);
     // NOTE: We cleared buffer but we don't know how many bytes are actually 
written
@@ -1794,7 +1863,13 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject 
*text)
     }
 
     if (needflush) {
-        if (_PyFile_Flush(self->buffer) < 0) {
+        PyObject *buffer = buffer_access_safe(self);
+        if (buffer == NULL) {
+            return NULL;
+        }
+        int res = _PyFile_Flush(buffer);
+        Py_DECREF(buffer);
+        if (res < 0) {
             return NULL;
         }
     }
@@ -1924,9 +1999,10 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t 
size_hint)
     if (chunk_size == NULL)
         goto fail;
 
-    input_chunk = PyObject_CallMethodOneArg(self->buffer,
-        (self->has_read1 ? &_Py_ID(read1): &_Py_ID(read)),
-        chunk_size);
+    input_chunk = buffer_callmethod_onearg(self,
+                                           (self->has_read1 ? &_Py_ID(read1) :
+                                                              &_Py_ID(read)),
+                                           chunk_size);
     Py_DECREF(chunk_size);
     if (input_chunk == NULL)
         goto fail;
@@ -2010,7 +2086,7 @@ _io_TextIOWrapper_read_impl(textio *self, Py_ssize_t n)
 
     if (n < 0) {
         /* Read everything */
-        PyObject *bytes = PyObject_CallMethodNoArgs(self->buffer, 
&_Py_ID(read));
+        PyObject *bytes = buffer_callmethod_noargs(self, &_Py_ID(read));
         PyObject *decoded;
         if (bytes == NULL)
             goto fail;
@@ -2607,7 +2683,12 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject 
*cookieObj, int whence)
             Py_DECREF(res);
         }
 
-        res = _PyObject_CallMethod(self->buffer, &_Py_ID(seek), "ii", 0, 2);
+        PyObject *buf = buffer_access_safe(self);
+        if (buf == NULL) {
+            goto fail;
+        }
+        res = _PyObject_CallMethod(buf, &_Py_ID(seek), "ii", 0, 2);
+        Py_DECREF(buf);
         Py_CLEAR(cookieObj);
         if (res == NULL)
             goto fail;
@@ -2655,7 +2736,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject 
*cookieObj, int whence)
     posobj = PyLong_FromOff_t(cookie.start_pos);
     if (posobj == NULL)
         goto fail;
-    res = PyObject_CallMethodOneArg(self->buffer, &_Py_ID(seek), posobj);
+    res = buffer_callmethod_onearg(self, &_Py_ID(seek), posobj);
     Py_DECREF(posobj);
     if (res == NULL)
         goto fail;
@@ -2672,8 +2753,15 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject 
*cookieObj, int whence)
 
     if (cookie.chars_to_skip) {
         /* Just like _read_chunk, feed the decoder and save a snapshot. */
-        PyObject *input_chunk = _PyObject_CallMethod(self->buffer, 
&_Py_ID(read),
-                                                     "i", 
cookie.bytes_to_feed);
+        PyObject *bytes_to_feed = PyLong_FromLong(cookie.bytes_to_feed);
+        if (bytes_to_feed == NULL) {
+            goto fail;
+        }
+        PyObject *input_chunk = buffer_callmethod_onearg(self,
+                                                         &_Py_ID(read),
+                                                         bytes_to_feed);
+        Py_DECREF(bytes_to_feed);
+
         PyObject *decoded;
 
         if (input_chunk == NULL)
@@ -2772,7 +2860,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
         goto fail;
     }
 
-    posobj = PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(tell));
+    posobj = buffer_callmethod_noargs(self, &_Py_ID(tell));
     if (posobj == NULL)
         goto fail;
 
@@ -2988,7 +3076,7 @@ _io_TextIOWrapper_truncate_impl(textio *self, PyObject 
*pos)
         return NULL;
     }
 
-    return PyObject_CallMethodOneArg(self->buffer, &_Py_ID(truncate), pos);
+    return buffer_callmethod_onearg(self, &_Py_ID(truncate), pos);
 }
 
 static PyObject *
@@ -3070,8 +3158,7 @@ static PyObject *
 _io_TextIOWrapper_fileno_impl(textio *self)
 /*[clinic end generated code: output=21490a4c3da13e6c input=515e1196aceb97ab]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(fileno));
+    return buffer_callmethod_noargs(self, &_Py_ID(fileno));
 }
 
 /*[clinic input]
@@ -3083,8 +3170,7 @@ static PyObject *
 _io_TextIOWrapper_seekable_impl(textio *self)
 /*[clinic end generated code: output=ab223dbbcffc0f00 input=71c4c092736c549b]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(seekable));
+    return buffer_callmethod_noargs(self, &_Py_ID(seekable));
 }
 
 /*[clinic input]
@@ -3096,8 +3182,7 @@ static PyObject *
 _io_TextIOWrapper_readable_impl(textio *self)
 /*[clinic end generated code: output=72ff7ba289a8a91b input=80438d1f01b0a89b]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(readable));
+    return buffer_callmethod_noargs(self, &_Py_ID(readable));
 }
 
 /*[clinic input]
@@ -3109,8 +3194,7 @@ static PyObject *
 _io_TextIOWrapper_writable_impl(textio *self)
 /*[clinic end generated code: output=a728c71790d03200 input=9d6c22befb0c340a]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(writable));
+    return buffer_callmethod_noargs(self, &_Py_ID(writable));
 }
 
 /*[clinic input]
@@ -3122,8 +3206,7 @@ static PyObject *
 _io_TextIOWrapper_isatty_impl(textio *self)
 /*[clinic end generated code: output=12be1a35bace882e input=7f83ff04d4d1733d]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(isatty));
+    return buffer_callmethod_noargs(self, &_Py_ID(isatty));
 }
 
 /*[clinic input]
@@ -3140,7 +3223,7 @@ _io_TextIOWrapper_flush_impl(textio *self)
     self->telling = self->seekable;
     if (_textiowrapper_writeflush(self) < 0)
         return NULL;
-    return PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(flush));
+    return buffer_callmethod_noargs(self, &_Py_ID(flush));
 }
 
 /*[clinic input]
@@ -3173,8 +3256,9 @@ _io_TextIOWrapper_close_impl(textio *self)
     else {
         PyObject *exc = NULL;
         if (self->finalizing) {
-            res = PyObject_CallMethodOneArg(self->buffer, 
&_Py_ID(_dealloc_warn),
-                                            (PyObject *)self);
+            res = buffer_callmethod_onearg(self,
+                                           &_Py_ID(_dealloc_warn),
+                                           (PyObject *)self);
             if (res) {
                 Py_DECREF(res);
             }
@@ -3186,7 +3270,7 @@ _io_TextIOWrapper_close_impl(textio *self)
             exc = PyErr_GetRaisedException();
         }
 
-        res = PyObject_CallMethodNoArgs(self->buffer, &_Py_ID(close));
+        res = buffer_callmethod_noargs(self, &_Py_ID(close));
         if (exc != NULL) {
             _PyErr_ChainExceptions1(exc);
             Py_CLEAR(res);
@@ -3254,8 +3338,7 @@ static PyObject *
 _io_TextIOWrapper_name_get_impl(textio *self)
 /*[clinic end generated code: output=8c2f1d6d8756af40 input=26ecec9b39e30e07]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_GetAttr(self->buffer, &_Py_ID(name));
+    return buffer_getattr(self, &_Py_ID(name));
 }
 
 /*[clinic input]
@@ -3268,8 +3351,17 @@ static PyObject *
 _io_TextIOWrapper_closed_get_impl(textio *self)
 /*[clinic end generated code: output=b49b68f443a85e3c input=7dfcf43f63c7003d]*/
 {
-    CHECK_ATTACHED(self);
-    return PyObject_GetAttr(self->buffer, &_Py_ID(closed));
+    /* If partially constructed or deconstructed, return that the underlying
+       buffer is closed.
+
+       The code managing the transition is responsible for closing. The closed
+       attribute is often called in re-initalization, as part of repr in error
+       cases, and when the I/O stack is garbage collected. */
+    if (self->ok <= 0) {
+        Py_RETURN_TRUE;
+    }
+
+    return buffer_getattr(self, &_Py_ID(closed));
 }
 
 /*[clinic input]
@@ -3344,6 +3436,19 @@ _io_TextIOWrapper__CHUNK_SIZE_set_impl(textio *self, 
PyObject *value)
     return 0;
 }
 
+/*[clinic input]
+@critical_section
+@getter
+_io.TextIOWrapper.buffer
+[clinic start generated code]*/
+
+static PyObject *
+_io_TextIOWrapper_buffer_get_impl(textio *self)
+/*[clinic end generated code: output=d265a34555aa5d4b input=5951cfa148f7350a]*/
+{
+    return buffer_access_safe(self);
+}
+
 static PyMethodDef incrementalnewlinedecoder_methods[] = {
     _IO_INCREMENTALNEWLINEDECODER_DECODE_METHODDEF
     _IO_INCREMENTALNEWLINEDECODER_GETSTATE_METHODDEF
@@ -3402,7 +3507,6 @@ static PyMethodDef textiowrapper_methods[] = {
 
 static PyMemberDef textiowrapper_members[] = {
     {"encoding", _Py_T_OBJECT, offsetof(textio, encoding), Py_READONLY},
-    {"buffer", _Py_T_OBJECT, offsetof(textio, buffer), Py_READONLY},
     {"line_buffering", Py_T_BOOL, offsetof(textio, line_buffering), 
Py_READONLY},
     {"write_through", Py_T_BOOL, offsetof(textio, write_through), Py_READONLY},
     {"_finalizing", Py_T_BOOL, offsetof(textio, finalizing), 0},
@@ -3417,6 +3521,7 @@ static PyGetSetDef textiowrapper_getset[] = {
     _IO_TEXTIOWRAPPER_NEWLINES_GETSETDEF
     _IO_TEXTIOWRAPPER_ERRORS_GETSETDEF
     _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF
+    _IO_TEXTIOWRAPPER_BUFFER_GETSETDEF
     {NULL}
 };
 

_______________________________________________
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