Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e1cc789

Browse files
authored
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 1b15c89)
1 parent 78a04b3 commit e1cc789

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

Lib/test/test_free_threading/test_dict.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ast import Or
77
from functools import partial
8-
from threading import Thread
8+
from threading import Thread, Barrier
99
from unittest import TestCase
1010

1111
try:
@@ -142,6 +142,25 @@ def writer_func(l):
142142
for ref in thread_list:
143143
self.assertIsNone(ref())
144144

145+
def test_getattr_setattr(self):
146+
NUM_THREADS = 10
147+
b = Barrier(NUM_THREADS)
148+
149+
def closure(b, c):
150+
b.wait()
151+
for i in range(10):
152+
getattr(c, f'attr_{i}', None)
153+
setattr(c, f'attr_{i}', 99)
154+
155+
class MyClass:
156+
pass
157+
158+
o = MyClass()
159+
threads = [Thread(target=closure, args=(b, o))
160+
for _ in range(NUM_THREADS)]
161+
with threading_helper.start_threads(threads):
162+
pass
163+
145164
@unittest.skipIf(_testcapi is None, 'need _testcapi module')
146165
def test_dict_version(self):
147166
dict_version = _testcapi.dict_version
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crash in the :term:`free threading` build when accessing an object
2+
attribute that may be concurrently inserted or deleted.

Objects/dictobject.c

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,37 @@ dictkeys_generic_lookup(PyDictObject *mp, PyDictKeysObject* dk, PyObject *key, P
11851185
return do_lookup(mp, dk, key, hash, compare_generic);
11861186
}
11871187

1188+
#ifdef Py_GIL_DISABLED
1189+
1190+
static Py_ssize_t
1191+
unicodekeys_lookup_unicode_threadsafe(PyDictKeysObject* dk, PyObject *key,
1192+
Py_hash_t hash);
1193+
1194+
#endif
1195+
1196+
static Py_ssize_t
1197+
unicodekeys_lookup_split(PyDictKeysObject* dk, PyObject *key, Py_hash_t hash)
1198+
{
1199+
Py_ssize_t ix;
1200+
assert(dk->dk_kind == DICT_KEYS_SPLIT);
1201+
assert(PyUnicode_CheckExact(key));
1202+
1203+
#ifdef Py_GIL_DISABLED
1204+
// A split dictionaries keys can be mutated by other dictionaries
1205+
// but if we have a unicode key we can avoid locking the shared
1206+
// keys.
1207+
ix = unicodekeys_lookup_unicode_threadsafe(dk, key, hash);
1208+
if (ix == DKIX_KEY_CHANGED) {
1209+
LOCK_KEYS(dk);
1210+
ix = unicodekeys_lookup_unicode(dk, key, hash);
1211+
UNLOCK_KEYS(dk);
1212+
}
1213+
#else
1214+
ix = unicodekeys_lookup_unicode(dk, key, hash);
1215+
#endif
1216+
return ix;
1217+
}
1218+
11881219
/* Lookup a string in a (all unicode) dict keys.
11891220
* Returns DKIX_ERROR if key is not a string,
11901221
* or if the dict keys is not all strings.
@@ -1209,13 +1240,24 @@ _PyDictKeys_StringLookup(PyDictKeysObject* dk, PyObject *key)
12091240
return unicodekeys_lookup_unicode(dk, key, hash);
12101241
}
12111242

1212-
#ifdef Py_GIL_DISABLED
1213-
1214-
static Py_ssize_t
1215-
unicodekeys_lookup_unicode_threadsafe(PyDictKeysObject* dk, PyObject *key,
1216-
Py_hash_t hash);
1217-
1218-
#endif
1243+
/* Like _PyDictKeys_StringLookup() but only works on split keys. Note
1244+
* that in free-threaded builds this locks the keys object as required.
1245+
*/
1246+
Py_ssize_t
1247+
_PyDictKeys_StringLookupSplit(PyDictKeysObject* dk, PyObject *key)
1248+
{
1249+
assert(dk->dk_kind == DICT_KEYS_SPLIT);
1250+
assert(PyUnicode_CheckExact(key));
1251+
Py_hash_t hash = unicode_get_hash(key);
1252+
if (hash == -1) {
1253+
hash = PyUnicode_Type.tp_hash(key);
1254+
if (hash == -1) {
1255+
PyErr_Clear();
1256+
return DKIX_ERROR;
1257+
}
1258+
}
1259+
return unicodekeys_lookup_split(dk, key, hash);
1260+
}
12191261

12201262
/*
12211263
The basic lookup function used by all operations.
@@ -6976,7 +7018,7 @@ _PyObject_TryGetInstanceAttribute(PyObject *obj, PyObject *name, PyObject **attr
69767018

69777019
PyDictKeysObject *keys = CACHED_KEYS(Py_TYPE(obj));
69787020
assert(keys != NULL);
6979-
Py_ssize_t ix = _PyDictKeys_StringLookup(keys, name);
7021+
Py_ssize_t ix = _PyDictKeys_StringLookupSplit(keys, name);
69807022
if (ix == DKIX_EMPTY) {
69817023
*attr = NULL;
69827024
return true;

0 commit comments

Comments
 (0)