diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 6b5244abe427cd..d24c8420ef8920 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 diff --git a/Lib/test/test_free_threading/test_io.py b/Lib/test/test_free_threading/test_io.py index e0bd7e211e7302..057e0adf3b42bc 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/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 00000000000000..d37e63e6116a59 --- /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/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 00000000000000..22604dd31dd605 --- /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/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 00000000000000..deea3fe66b9b5a --- /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 3d14ec3f8f94a9..7d6053d85cd9e4 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)); } } diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index b8601383ad0a26..27605b4c44239e 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 diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 71d946a637df1c..348c7dd6967a39 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; }