Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Doc/library/asyncio-eventloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions Lib/test/test_free_threading/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion Modules/_io/bytesio.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down
14 changes: 13 additions & 1 deletion Modules/_io/stringio.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading