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