https://github.com/python/cpython/commit/60e956e18b6d23de777f964010b11adddf147750
commit: 60e956e18b6d23de777f964010b11adddf147750
branch: main
author: Edward Xu <[email protected]>
committer: DinoV <[email protected]>
date: 2026-10-22T10:44:20-07:00
summary:
gh-149816: Fix race between `setattr` and `object.__dict__` update (#152296)
Fix race between `setattr` and `object.__dict__` update (#152296)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-00-02-29.gh-issue-149816.JKN-5B.rst
M Lib/test/test_free_threading/test_dict.py
M Objects/dictobject.c
diff --git a/Lib/test/test_free_threading/test_dict.py
b/Lib/test/test_free_threading/test_dict.py
index 2180613f689c89..12cea4b98a1724 100644
--- a/Lib/test/test_free_threading/test_dict.py
+++ b/Lib/test/test_free_threading/test_dict.py
@@ -437,5 +437,61 @@ def writer():
threading_helper.run_concurrently([reader, writer])
+ def test_racing_dict_update_and_method_lookup_with_inline_values(self):
+ # gh-149816: sub-case 108
+ # The race below checks that a detached dict is still valid
+ # when racing setattr with __dict__ replacement
+ class Target:
+ pass
+
+ def appender(obj: Target, start: Barrier, iter_times: int) -> None:
+ start.wait()
+ index = 0
+ for _ in range(iter_times):
+ setattr(obj, f"probe_{index}", index)
+ index += 1
+ time.sleep(0)
+
+ def replacer(obj: Target, start: Barrier, iter_times: int, churn_size:
int) -> None:
+ start.wait()
+ for _ in range(iter_times):
+ old_dict = obj.__dict__
+ obj.__dict__ = {}
+ del old_dict
+ time.sleep(0)
+ # create a list of dicts to trigger a realloc of the dict's
table
+ # and ensure that the old dict is not used after it is deleted
+ realloc_trigger_list = [{"k": j} for j in range(churn_size)]
+ del realloc_trigger_list
+
+ def race(iter_times: int,
+ churn_size: int,
+ appender_threads: int,
+ replacer_threads: int) -> None:
+ obj = Target()
+ setattr(obj, "origin", 0)
+ _ = obj.__dict__ # Access __dict__ to ensure it's initialized
+
+ start = Barrier(appender_threads + replacer_threads)
+ threads = []
+ for _ in range(appender_threads):
+ threads.append(Thread(target=appender, args=(obj, start,
iter_times),
+ name="appender"))
+ for _ in range(replacer_threads):
+ threads.append(Thread(target=replacer, args=(obj, start,
iter_times, churn_size),
+ name="replacer"))
+
+ with threading_helper.catch_threading_exception() as cm:
+ with threading_helper.start_threads(threads):
+ pass
+ if cm.exc_type is not None:
+ raise cm.exc_value
+
+ ITER_TIMES = 2_000
+ APPENDER_THREADS = 8
+ REPLACER_THREADS = 8
+ CHURN_SIZE = 512
+ race(ITER_TIMES, CHURN_SIZE, APPENDER_THREADS, REPLACER_THREADS)
+
if __name__ == "__main__":
unittest.main()
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-00-02-29.gh-issue-149816.JKN-5B.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-00-02-29.gh-issue-149816.JKN-5B.rst
new file mode 100644
index 00000000000000..255507548703b1
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-00-02-29.gh-issue-149816.JKN-5B.rst
@@ -0,0 +1,3 @@
+Fix a race in free-threaded builds where storing an instance attribute could
+use a stale borrowed reference to an instance ``__dict__`` after another thread
+replaced it.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 41b90f9277d0f2..e7ec0fd7c0e56e 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -7572,17 +7572,25 @@ _PyObject_StoreInstanceAttribute(PyObject *obj,
PyObject *name, PyObject *value)
{
PyDictValues *values = _PyObject_InlineValues(obj);
if (!FT_ATOMIC_LOAD_UINT8(values->valid)) {
- PyDictObject *dict = _PyObject_GetManagedDict(obj);
+ PyDictObject *dict;
+ int res;
+#ifdef Py_GIL_DISABLED
+ Py_BEGIN_CRITICAL_SECTION(obj);
+#endif
+ dict = _PyObject_GetManagedDict(obj);
+ Py_XINCREF(dict);
+#ifdef Py_GIL_DISABLED
+ Py_END_CRITICAL_SECTION();
+#endif
if (dict == NULL) {
dict = (PyDictObject *)PyObject_GenericGetDict(obj, NULL);
if (dict == NULL) {
return -1;
}
- int res = store_instance_attr_dict(obj, dict, name, value);
- Py_DECREF(dict);
- return res;
}
- return store_instance_attr_dict(obj, dict, name, value);
+ res = store_instance_attr_dict(obj, dict, name, value);
+ Py_DECREF(dict);
+ return res;
}
#ifdef Py_GIL_DISABLED
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]