https://github.com/python/cpython/commit/881a13a97c7a29b9de57913c773cd5f195270fbd
commit: 881a13a97c7a29b9de57913c773cd5f195270fbd
branch: main
author: Pedram Karimi <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-07-05T21:17:20-04:00
summary:

gh-151640: Track sharing of `BytesIO` buffer in free-threaded builds (GH-151651)

files:
A Misc/NEWS.d/next/Library/2026-06-18-12-48-03.gh-issue-151640.R4c3Fx.rst
M Lib/test/test_free_threading/test_io.py
M Lib/test/test_io/test_memoryio.py
M Modules/_io/bytesio.c

diff --git a/Lib/test/test_free_threading/test_io.py 
b/Lib/test/test_free_threading/test_io.py
index 742da6f8c788b42..e0bd7e211e73024 100644
--- a/Lib/test/test_free_threading/test_io.py
+++ b/Lib/test/test_free_threading/test_io.py
@@ -122,6 +122,35 @@ def sizeof(barrier, b, *ignore):
 class CBytesIOTest(ThreadSafetyMixin, TestCase):
     ioclass = io.BytesIO
 
+    @threading_helper.requires_working_threading()
+    @threading_helper.reap_threads
+    def test_concurrent_whole_buffer_read_and_resize(self):
+        shared = self.ioclass(b"x" * 64)
+        writers = 2
+        readers = 8
+        loops = 2000
+        barrier = threading.Barrier(writers + readers)
+
+        def writer():
+            barrier.wait()
+            for i in range(loops):
+                shared.seek(0)
+                shared.write(b"a" * (64 + (i & 63)))
+
+        def reader():
+            barrier.wait()
+            for _ in range(loops):
+                shared.seek(0)
+                shared.read()
+                shared.seek(0)
+                shared.peek()
+                shared.getvalue()
+
+        threads = [threading.Thread(target=writer) for _ in range(writers)]
+        threads += [threading.Thread(target=reader) for _ in range(readers)]
+        with threading_helper.start_threads(threads):
+            pass
+
 class PyBytesIOTest(ThreadSafetyMixin, TestCase):
      ioclass = pyio.BytesIO
 
diff --git a/Lib/test/test_io/test_memoryio.py 
b/Lib/test/test_io/test_memoryio.py
index 5a93d2634580461..f53525d77b7dbaf 100644
--- a/Lib/test/test_io/test_memoryio.py
+++ b/Lib/test/test_io/test_memoryio.py
@@ -939,7 +939,10 @@ def test_setstate(self):
 
     @support.cpython_only
     def test_sizeof(self):
-        basesize = support.calcobjsize('P2n2Pn')
+        if support.Py_GIL_DISABLED:
+            basesize = support.calcobjsize('P2n2Pni')
+        else:
+            basesize = support.calcobjsize('P2n2Pn')
         check = self.check_sizeof
         self.assertEqual(object.__sizeof__(io.BytesIO()), basesize)
         check(io.BytesIO(), basesize )
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-18-12-48-03.gh-issue-151640.R4c3Fx.rst 
b/Misc/NEWS.d/next/Library/2026-06-18-12-48-03.gh-issue-151640.R4c3Fx.rst
new file mode 100644
index 000000000000000..ef61a7d27dd07dd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-18-12-48-03.gh-issue-151640.R4c3Fx.rst
@@ -0,0 +1,3 @@
+Fix a data race in :class:`io.BytesIO` in free-threaded builds when
+whole-buffer reads or peeks, or :meth:`~io.BytesIO.getvalue`, share the
+internal buffer with concurrent writes.
diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c
index ffe82150be1b514..3d14ec3f8f94a92 100644
--- a/Modules/_io/bytesio.c
+++ b/Modules/_io/bytesio.c
@@ -22,6 +22,9 @@ typedef struct {
     PyObject *dict;
     PyObject *weakreflist;
     Py_ssize_t exports;
+#ifdef Py_GIL_DISABLED
+    int buf_shared;
+#endif
 } bytesio;
 
 #define bytesio_CAST(op)    ((bytesio *)(op))
@@ -71,7 +74,45 @@ check_exports(bytesio *self)
         return NULL; \
     }
 
+#ifdef Py_GIL_DISABLED
+#define SHARED_BUF(self) ((self)->buf_shared || 
!_PyObject_IsUniquelyReferenced((self)->buf))
+#else
 #define SHARED_BUF(self) (!_PyObject_IsUniquelyReferenced((self)->buf))
+#endif
+
+static inline void
+set_shared_buf(bytesio *self)
+{
+#ifdef Py_GIL_DISABLED
+    self->buf_shared = 1;
+#endif
+}
+
+static inline void
+clear_shared_buf(bytesio *self)
+{
+#ifdef Py_GIL_DISABLED
+    self->buf_shared = 0;
+#endif
+}
+
+static int
+resize_unshared_buffer_lock_held(bytesio *self, Py_ssize_t size)
+{
+    _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
+
+#ifdef Py_GIL_DISABLED
+    /* If the internal bytes object escaped via a zero-copy getvalue(), read(),
+       or peek(), resizing it would mutate an object visible to Python code.
+       Callers must detach first. */
+    assert(!self->buf_shared);
+#endif
+    int ret = _PyBytes_Resize(&self->buf, size);
+    if (ret == 0) {
+        clear_shared_buf(self);
+    }
+    return ret;
+}
 
 
 /* Internal routine to get a line from the buffer of a BytesIO
@@ -128,6 +169,7 @@ unshare_buffer_lock_held(bytesio *self, size_t size)
     memcpy(PyBytes_AS_STRING(new_buf), PyBytes_AS_STRING(self->buf),
            self->string_size);
     Py_SETREF(self->buf, new_buf);
+    clear_shared_buf(self);
     return 0;
 }
 
@@ -173,7 +215,7 @@ resize_buffer_lock_held(bytesio *self, size_t size)
             return -1;
     }
     else {
-        if (_PyBytes_Resize(&self->buf, alloc) < 0)
+        if (resize_unshared_buffer_lock_held(self, alloc) < 0)
             return -1;
     }
 
@@ -381,10 +423,11 @@ _io_BytesIO_getvalue_impl(bytesio *self)
                 return NULL;
         }
         else {
-            if (_PyBytes_Resize(&self->buf, self->string_size) < 0)
+            if (resize_unshared_buffer_lock_held(self, self->string_size) < 0)
                 return NULL;
         }
     }
+    set_shared_buf(self);
     return Py_NewRef(self->buf);
 }
 
@@ -433,6 +476,7 @@ peek_bytes_lock_held(bytesio *self, Py_ssize_t size)
     if (size > 1 &&
         self->pos == 0 && size == PyBytes_GET_SIZE(self->buf) &&
         FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) == 0) {
+        set_shared_buf(self);
         return Py_NewRef(self->buf);
     }
 
@@ -1091,6 +1135,7 @@ _io_BytesIO___init___impl(bytesio *self, PyObject 
*initvalue)
     if (initvalue && initvalue != Py_None) {
         if (PyBytes_CheckExact(initvalue)) {
             Py_XSETREF(self->buf, Py_NewRef(initvalue));
+            clear_shared_buf(self);
             self->string_size = PyBytes_GET_SIZE(initvalue);
         }
         else {

_______________________________________________
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