From 6c026ad6c49cb7e9fa4a0867f2d0b4071a37a483 Mon Sep 17 00:00:00 2001 From: Bhuvi Date: Sat, 25 Jul 2026 11:26:33 +0530 Subject: [PATCH 1/4] gh-153290: Fix data race in BytesIO.__setstate__ installing __dict__ (#153376) --- Lib/test/test_free_threading/test_io.py | 28 +++++++++++++++++++ ...-07-09-08-40-00.gh-issue-153290.GTzSEa.rst | 3 ++ Modules/_io/bytesio.c | 4 ++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-09-08-40-00.gh-issue-153290.GTzSEa.rst diff --git a/Lib/test/test_free_threading/test_io.py b/Lib/test/test_free_threading/test_io.py index e0bd7e211e73024..057e0adf3b42bc4 100644 --- a/Lib/test/test_free_threading/test_io.py +++ b/Lib/test/test_free_threading/test_io.py @@ -122,6 +122,34 @@ def sizeof(barrier, b, *ignore): class CBytesIOTest(ThreadSafetyMixin, TestCase): ioclass = io.BytesIO + @threading_helper.requires_working_threading() + @threading_helper.reap_threads + def test_concurrent_setstate_and_method_call(self): + # gh-153290: __setstate__() installed the instance __dict__ with a + # plain store, racing the lock-free LOAD_ATTR method fast path that + # reads the dict slot with an atomic acquire load. + states = [(b"A" * 64, 0, {}), (b"B" * 128, 32, {}), (b"C" * 256, 0, {})] + nreaders = 4 + for _ in range(25): + shared = self.ioclass(b"initial payload") + barrier = threading.Barrier(1 + nreaders) + + def setter(): + barrier.wait() + for state in states: + shared.__setstate__(state) + + def reader(): + barrier.wait() + for _ in range(100): + shared.read(8) + + threads = [threading.Thread(target=setter)] + threads += [threading.Thread(target=reader) + for _ in range(nreaders)] + with threading_helper.start_threads(threads): + pass + @threading_helper.requires_working_threading() @threading_helper.reap_threads def test_concurrent_whole_buffer_read_and_resize(self): diff --git a/Misc/NEWS.d/next/Library/2026-07-09-08-40-00.gh-issue-153290.GTzSEa.rst b/Misc/NEWS.d/next/Library/2026-07-09-08-40-00.gh-issue-153290.GTzSEa.rst new file mode 100644 index 000000000000000..deea3fe66b9b5af --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-09-08-40-00.gh-issue-153290.GTzSEa.rst @@ -0,0 +1,3 @@ +Fix a data race on the free-threaded build when :meth:`!io.BytesIO.__setstate__` +installs the instance dictionary while another thread concurrently calls a +method on the same object. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 3d14ec3f8f94a92..7d6053d85cd9e4a 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -1054,7 +1054,9 @@ bytesio_setstate_lock_held(PyObject *op, PyObject *state) return NULL; } else { - self->dict = Py_NewRef(dict); + /* The LOAD_ATTR specializations read the dict slot lock-free + with an acquire load, so pair it with a release store. */ + FT_ATOMIC_STORE_PTR_RELEASE(self->dict, Py_NewRef(dict)); } } From 222a8bf3259b6fae2132c9278aa9ecff68a99b2a Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 25 Jul 2026 12:10:22 +0530 Subject: [PATCH 2/4] gh-120866: Document behavior change of asyncio.Server.wait_closed() since 3.12 (#154676) --- Doc/library/asyncio-eventloop.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 6b5244abe427cd3..d24c8420ef8920c 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -1873,6 +1873,12 @@ Do not instantiate the :class:`Server` class directly. Wait until the :meth:`close` method completes and all active connections have finished. + .. versionchanged:: 3.12 + ``wait_closed()`` now waits until the server is closed and + all active connections have finished. Previously, it returned + immediately if the server was already closed, even if + connections were still active. + .. attribute:: sockets List of socket-like objects, ``asyncio.trsock.TransportSocket``, which From e31d30266f33198d92e5adbbc055f6595555726d Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Sat, 25 Jul 2026 16:29:59 +0800 Subject: [PATCH 3/4] gh-154043: Fix a data race when iterating a shared `types.GenericAlias` iterator in FT build (#154108) --- .../2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst | 2 ++ Objects/genericaliasobject.c | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst new file mode 100644 index 000000000000000..d37e63e6116a593 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst @@ -0,0 +1,2 @@ +Fix a data race when iterating a shared :class:`types.GenericAlias` iterator +from multiple threads under the :term:`free-threaded build`. diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 71d946a637df1c9..348c7dd6967a397 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -942,17 +942,23 @@ static PyObject * ga_iternext(PyObject *op) { gaiterobject *gi = (gaiterobject*)op; - if (gi->obj == NULL) { +#ifdef Py_GIL_DISABLED + PyObject *obj = _Py_atomic_exchange_ptr(&gi->obj, NULL); +#else + PyObject* obj = gi->obj; + gi->obj = NULL; +#endif + if (obj == NULL) { PyErr_SetNone(PyExc_StopIteration); return NULL; } - gaobject *alias = (gaobject *)gi->obj; + gaobject *alias = (gaobject *)obj; PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args); + Py_DECREF(obj); if (starred_alias == NULL) { return NULL; } ((gaobject *)starred_alias)->starred = true; - Py_SETREF(gi->obj, NULL); return starred_alias; } From bff005123119d5a010774f5dc9a181029024c2ac Mon Sep 17 00:00:00 2001 From: Rishit Agnihotri <67520802+Twix1288@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:35:25 -0700 Subject: [PATCH 4/4] gh-153296: Fix thread-safety data race in io.StringIO iterator (#153368) --- .../2026-07-08-15-26-00.gh-issue-153296.Twix12.rst | 1 + Modules/_io/stringio.c | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-08-15-26-00.gh-issue-153296.Twix12.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-08-15-26-00.gh-issue-153296.Twix12.rst b/Misc/NEWS.d/next/Library/2026-07-08-15-26-00.gh-issue-153296.Twix12.rst new file mode 100644 index 000000000000000..22604dd31dd6050 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-08-15-26-00.gh-issue-153296.Twix12.rst @@ -0,0 +1 @@ +Fix a data race and use-after-free when iterating over an :class:`io.StringIO` object while it is being concurrently mutated. The ``__next__`` method now properly acquires the object's lock. diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index b8601383ad0a26f..27605b4c44239e9 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -407,8 +407,10 @@ _io_StringIO_readline_impl(stringio *self, Py_ssize_t size) } static PyObject * -stringio_iternext(PyObject *op) +stringio_iternext_lock_held(PyObject *op) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); + PyObject *line; stringio *self = stringio_CAST(op); @@ -444,6 +446,16 @@ stringio_iternext(PyObject *op) return line; } +static PyObject * +stringio_iternext(PyObject *op) +{ + PyObject *ret; + Py_BEGIN_CRITICAL_SECTION(op); + ret = stringio_iternext_lock_held(op); + Py_END_CRITICAL_SECTION(); + return ret; +} + /*[clinic input] @critical_section _io.StringIO.truncate