https://github.com/python/cpython/commit/e1cc7895313d143478040c9cf8e2215402e326fc
commit: e1cc7895313d143478040c9cf8e2215402e326fc
branch: 3.13
author: Sam Gross <[email protected]>
committer: methane <[email protected]>
date: 2025-05-14T12:47:34+09:00
summary:
gh-132869: Fix crash in `_PyObject_TryGetInstanceAttribute` (#133700)
This fixes a crash in `_PyObject_TryGetInstanceAttribute` due to the use
of `_PyDictKeys_StringLookup` on an unlocked dictionary that may be
concurrently modified.
The underlying bug was already fixed in 3.14 and the main branch.
(partially cherry picked from commit 1b15c89a17ca3de6b05de5379b8717e9738c51ef)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-05-08-19-01-32.gh-issue-132869.lqIOhZ.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 3126458e08e50a..af6c8b697434ff 100644
--- a/Lib/test/test_free_threading/test_dict.py
+++ b/Lib/test/test_free_threading/test_dict.py
@@ -5,7 +5,7 @@
from ast import Or
from functools import partial
-from threading import Thread
+from threading import Thread, Barrier
from unittest import TestCase
try:
@@ -142,6 +142,25 @@ def writer_func(l):
for ref in thread_list:
self.assertIsNone(ref())
+ def test_getattr_setattr(self):
+ NUM_THREADS = 10
+ b = Barrier(NUM_THREADS)
+
+ def closure(b, c):
+ b.wait()
+ for i in range(10):
+ getattr(c, f'attr_{i}', None)
+ setattr(c, f'attr_{i}', 99)
+
+ class MyClass:
+ pass
+
+ o = MyClass()
+ threads = [Thread(target=closure, args=(b, o))
+ for _ in range(NUM_THREADS)]
+ with threading_helper.start_threads(threads):
+ pass
+
@unittest.skipIf(_testcapi is None, 'need _testcapi module')
def test_dict_version(self):
dict_version = _testcapi.dict_version
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-08-19-01-32.gh-issue-132869.lqIOhZ.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-08-19-01-32.gh-issue-132869.lqIOhZ.rst
new file mode 100644
index 00000000000000..88fbdc14a9c48e
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-08-19-01-32.gh-issue-132869.lqIOhZ.rst
@@ -0,0 +1,2 @@
+Fix crash in the :term:`free threading` build when accessing an object
+attribute that may be concurrently inserted or deleted.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 501f3d2d2bcaf5..d135ea5f3db81b 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -1185,6 +1185,37 @@ dictkeys_generic_lookup(PyDictObject *mp,
PyDictKeysObject* dk, PyObject *key, P
return do_lookup(mp, dk, key, hash, compare_generic);
}
+#ifdef Py_GIL_DISABLED
+
+static Py_ssize_t
+unicodekeys_lookup_unicode_threadsafe(PyDictKeysObject* dk, PyObject *key,
+ Py_hash_t hash);
+
+#endif
+
+static Py_ssize_t
+unicodekeys_lookup_split(PyDictKeysObject* dk, PyObject *key, Py_hash_t hash)
+{
+ Py_ssize_t ix;
+ assert(dk->dk_kind == DICT_KEYS_SPLIT);
+ assert(PyUnicode_CheckExact(key));
+
+#ifdef Py_GIL_DISABLED
+ // A split dictionaries keys can be mutated by other dictionaries
+ // but if we have a unicode key we can avoid locking the shared
+ // keys.
+ ix = unicodekeys_lookup_unicode_threadsafe(dk, key, hash);
+ if (ix == DKIX_KEY_CHANGED) {
+ LOCK_KEYS(dk);
+ ix = unicodekeys_lookup_unicode(dk, key, hash);
+ UNLOCK_KEYS(dk);
+ }
+#else
+ ix = unicodekeys_lookup_unicode(dk, key, hash);
+#endif
+ return ix;
+}
+
/* Lookup a string in a (all unicode) dict keys.
* Returns DKIX_ERROR if key is not a string,
* or if the dict keys is not all strings.
@@ -1209,13 +1240,24 @@ _PyDictKeys_StringLookup(PyDictKeysObject* dk, PyObject
*key)
return unicodekeys_lookup_unicode(dk, key, hash);
}
-#ifdef Py_GIL_DISABLED
-
-static Py_ssize_t
-unicodekeys_lookup_unicode_threadsafe(PyDictKeysObject* dk, PyObject *key,
- Py_hash_t hash);
-
-#endif
+/* Like _PyDictKeys_StringLookup() but only works on split keys. Note
+ * that in free-threaded builds this locks the keys object as required.
+ */
+Py_ssize_t
+_PyDictKeys_StringLookupSplit(PyDictKeysObject* dk, PyObject *key)
+{
+ assert(dk->dk_kind == DICT_KEYS_SPLIT);
+ assert(PyUnicode_CheckExact(key));
+ Py_hash_t hash = unicode_get_hash(key);
+ if (hash == -1) {
+ hash = PyUnicode_Type.tp_hash(key);
+ if (hash == -1) {
+ PyErr_Clear();
+ return DKIX_ERROR;
+ }
+ }
+ return unicodekeys_lookup_split(dk, key, hash);
+}
/*
The basic lookup function used by all operations.
@@ -6976,7 +7018,7 @@ _PyObject_TryGetInstanceAttribute(PyObject *obj, PyObject
*name, PyObject **attr
PyDictKeysObject *keys = CACHED_KEYS(Py_TYPE(obj));
assert(keys != NULL);
- Py_ssize_t ix = _PyDictKeys_StringLookup(keys, name);
+ Py_ssize_t ix = _PyDictKeys_StringLookupSplit(keys, name);
if (ix == DKIX_EMPTY) {
*attr = NULL;
return true;
_______________________________________________
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]