https://github.com/python/cpython/commit/e1cc7895313d143478040c9cf8e2215402e326fc
commit: e1cc7895313d143478040c9cf8e2215402e326fc
branch: 3.13
author: Sam Gross <colesb...@gmail.com>
committer: methane <songofaca...@gmail.com>
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 -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to