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

kumaraditya303 <[email protected]> Tue, 04 Aug 2026 09:30:08 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/8440a6a4651f93ecd1c7768ffb2c735c5b9fb984
commit: 8440a6a4651f93ecd1c7768ffb2c735c5b9fb984
branch: main
author: Brij Kapadia <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-04T18:59:52+05:30
summary:

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

files:
A Misc/NEWS.d/next/Library/2026-07-23-18-53-48.gh-issue-154523.XUSRBO.rst
M Lib/test/test_free_threading/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 057e0adf3b42bc4..a2b1ec8eb72bb1a 100644
--- a/Lib/test/test_free_threading/test_io.py
+++ b/Lib/test/test_free_threading/test_io.py
@@ -232,3 +232,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/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/Modules/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h
index 8d59bda5f74b386..3c682cb2f271aef 100644
--- a/Modules/_io/clinic/textio.c.h
+++ b/Modules/_io/clinic/textio.c.h
@@ -1331,4 +1331,29 @@ _io_TextIOWrapper__CHUNK_SIZE_set(PyObject *self, PyObject *value, void *Py_UNUS
 
     return return_value;
 }
-/*[clinic end generated code: output=8c571c9dba87d2b1 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=e34c75e1d2a12084 input=a9049054013a1b77]*/
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index 5b2a20a30c28cb2..ea8ed2713d8a146 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -3424,6 +3424,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 Py_XNewRef(buffer_access_safe(self));
+}
+
 static PyMethodDef incrementalnewlinedecoder_methods[] = {
     _IO_INCREMENTALNEWLINEDECODER_DECODE_METHODDEF
     _IO_INCREMENTALNEWLINEDECODER_GETSTATE_METHODDEF
@@ -3482,7 +3495,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},
@@ -3497,6 +3509,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]