Skip to content

Commit 6c026ad

Browse files
authored
gh-153290: Fix data race in BytesIO.__setstate__ installing __dict__ (#153376)
1 parent 5062427 commit 6c026ad

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

Lib/test/test_free_threading/test_io.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ def sizeof(barrier, b, *ignore):
122122
class CBytesIOTest(ThreadSafetyMixin, TestCase):
123123
ioclass = io.BytesIO
124124

125+
@threading_helper.requires_working_threading()
126+
@threading_helper.reap_threads
127+
def test_concurrent_setstate_and_method_call(self):
128+
# gh-153290: __setstate__() installed the instance __dict__ with a
129+
# plain store, racing the lock-free LOAD_ATTR method fast path that
130+
# reads the dict slot with an atomic acquire load.
131+
states = [(b"A" * 64, 0, {}), (b"B" * 128, 32, {}), (b"C" * 256, 0, {})]
132+
nreaders = 4
133+
for _ in range(25):
134+
shared = self.ioclass(b"initial payload")
135+
barrier = threading.Barrier(1 + nreaders)
136+
137+
def setter():
138+
barrier.wait()
139+
for state in states:
140+
shared.__setstate__(state)
141+
142+
def reader():
143+
barrier.wait()
144+
for _ in range(100):
145+
shared.read(8)
146+
147+
threads = [threading.Thread(target=setter)]
148+
threads += [threading.Thread(target=reader)
149+
for _ in range(nreaders)]
150+
with threading_helper.start_threads(threads):
151+
pass
152+
125153
@threading_helper.requires_working_threading()
126154
@threading_helper.reap_threads
127155
def test_concurrent_whole_buffer_read_and_resize(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a data race on the free-threaded build when :meth:`!io.BytesIO.__setstate__`
2+
installs the instance dictionary while another thread concurrently calls a
3+
method on the same object.

Modules/_io/bytesio.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,9 @@ bytesio_setstate_lock_held(PyObject *op, PyObject *state)
10541054
return NULL;
10551055
}
10561056
else {
1057-
self->dict = Py_NewRef(dict);
1057+
/* The LOAD_ATTR specializations read the dict slot lock-free
1058+
with an acquire load, so pair it with a release store. */
1059+
FT_ATOMIC_STORE_PTR_RELEASE(self->dict, Py_NewRef(dict));
10581060
}
10591061
}
10601062

0 commit comments

Comments
 (0)