diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index ad23290a92ab345..4a812275143bc4d 100644 --- a/Lib/test/test_free_threading/test_dict.py +++ b/Lib/test/test_free_threading/test_dict.py @@ -336,5 +336,25 @@ def reader(): with threading_helper.start_threads([t1, t2]): pass + def test_getstate_race_with_shared_keys(self): + box = [None] + enter = Barrier(2) + leave = Barrier(2) + + def reader(): + for _ in range(1000): + enter.wait() + box[0].__getstate__() + leave.wait() + + def writer(): + for i in range(1000): + box[0] = type(f"C{i}", (), {})() + enter.wait() + setattr(box[0], str(i), 1) + leave.wait() + + threading_helper.run_concurrently([reader, writer]) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-17-22-03-39.gh-issue-153881.oDa06s.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-17-22-03-39.gh-issue-153881.oDa06s.rst new file mode 100644 index 000000000000000..8facf88367fda7f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-17-22-03-39.gh-issue-153881.oDa06s.rst @@ -0,0 +1,2 @@ +Fix potential data race when calling :meth:`~object.__getstate__` +under the :term:`free-threaded build`. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 246ffe6c18e9b51..c650aa456d2cc9d 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7722,7 +7722,7 @@ _PyObject_IsInstanceDictEmpty(PyObject *obj) PyDictValues *values = _PyObject_InlineValues(obj); if (FT_ATOMIC_LOAD_UINT8(values->valid)) { PyDictKeysObject *keys = CACHED_KEYS(tp); - for (Py_ssize_t i = 0; i < keys->dk_nentries; i++) { + for (Py_ssize_t i = 0; i < LOAD_KEYS_NENTRIES(keys); i++) { if (FT_ATOMIC_LOAD_PTR_RELAXED(values->values[i]) != NULL) { return 0; }