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)); } }