Skip to content

Commit 2f8aa46

Browse files
committed
gh-149816: Fix another variant of race on setattr/managed dict going away
1 parent 60e956e commit 2f8aa46

2 files changed

Lines changed: 166 additions & 117 deletions

File tree

‎Lib/test/test_free_threading/test_dict.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,5 +493,55 @@ def race(iter_times: int,
493493
CHURN_SIZE = 512
494494
race(ITER_TIMES, CHURN_SIZE, APPENDER_THREADS, REPLACER_THREADS)
495495

496+
def test_racing_setattr_and_dict_replace_with_valid_inline_values(self):
497+
# gh-149816: setattr on an object whose materialized __dict__ still
498+
# points at valid inline values, racing with __dict__ replacement
499+
# which detaches and frees the old dict.
500+
class Target:
501+
pass
502+
503+
ROUNDS = 50
504+
BATCH = 200
505+
SETTER_THREADS = 4
506+
REPLACER_THREADS = 4
507+
508+
objs = []
509+
barrier = Barrier(SETTER_THREADS + REPLACER_THREADS + 1)
510+
511+
def setter(i):
512+
for _ in range(ROUNDS):
513+
barrier.wait()
514+
for obj in objs:
515+
obj.x = i
516+
obj.y = i
517+
barrier.wait()
518+
519+
def replacer():
520+
for _ in range(ROUNDS):
521+
barrier.wait()
522+
for obj in objs:
523+
# Materialize the dict while the inline values are valid
524+
obj.__dict__
525+
obj.__dict__ = {}
526+
barrier.wait()
527+
528+
threads = [Thread(target=setter, args=(i,))
529+
for i in range(SETTER_THREADS)]
530+
threads += [Thread(target=replacer) for _ in range(REPLACER_THREADS)]
531+
532+
with threading_helper.catch_threading_exception() as cm:
533+
with threading_helper.start_threads(threads):
534+
for _ in range(ROUNDS):
535+
new_objs = []
536+
for _ in range(BATCH):
537+
obj = Target()
538+
obj.a = 1
539+
new_objs.append(obj)
540+
objs[:] = new_objs
541+
barrier.wait()
542+
barrier.wait()
543+
if cm.exc_type is not None:
544+
raise cm.exc_value
545+
496546
if __name__ == "__main__":
497547
unittest.main()

‎Objects/dictobject.c‎

Lines changed: 116 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,12 @@ insertdict(PyDictObject *mp,
20592059

20602060
if (_PyDict_HasSplitTable(mp) && PyUnicode_CheckExact(key)) {
20612061
ix = insert_split_key(mp->ma_keys, key, hash);
2062+
#ifdef Py_GIL_DISABLED
2063+
if (!_PyDict_HasSplitTable(mp)) {
2064+
ix = _Py_dict_lookup(mp, key, hash, &old_value);
2065+
}
2066+
else
2067+
#endif
20622068
if (ix != DKIX_EMPTY) {
20632069
_PyDict_InsertSplitValue(mp, key, value, ix);
20642070
Py_DECREF(key);
@@ -7462,55 +7468,58 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values,
74627468
assert(values != NULL);
74637469
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES);
74647470
Py_ssize_t ix = DKIX_EMPTY;
7465-
PyDictObject *dict = _PyObject_GetManagedDict(obj);
7466-
assert(dict == NULL || ((PyDictObject *)dict)->ma_values == values);
7467-
if (PyUnicode_CheckExact(name)) {
7468-
Py_hash_t hash = unicode_get_hash(name);
7469-
if (hash == -1) {
7470-
hash = PyUnicode_Type.tp_hash(name);
7471-
assert(hash != -1);
7472-
}
7471+
assert(!_PyObject_GetManagedDict(obj));
7472+
assert(((PyDictObject *)dict)->ma_values == values);
7473+
assert(PyUnicode_CheckExact(name));
74737474

7474-
ix = insert_split_key(keys, name, hash);
7475+
Py_hash_t hash = unicode_get_hash(name);
7476+
if (hash == -1) {
7477+
hash = PyUnicode_Type.tp_hash(name);
7478+
assert(hash != -1);
7479+
}
74757480

7476-
#ifdef Py_STATS
7477-
if (ix == DKIX_EMPTY) {
7478-
if (PyUnicode_CheckExact(name)) {
7479-
if (shared_keys_usable_size(keys) == SHARED_KEYS_MAX_SIZE) {
7480-
OBJECT_STAT_INC(dict_materialized_too_big);
7481-
}
7482-
else {
7483-
OBJECT_STAT_INC(dict_materialized_new_key);
7484-
}
7485-
}
7486-
else {
7487-
OBJECT_STAT_INC(dict_materialized_str_subclass);
7488-
}
7481+
#ifdef Py_GIL_DISABLED
7482+
ix = unicodekeys_lookup_unicode_threadsafe(keys, name, hash);
7483+
if (ix < 0) {
7484+
// insert split key locks which can release our object lock opening
7485+
// up an opportunity to create the dict.
7486+
ix = insert_split_key(keys, name, hash);
7487+
PyDictObject *dict = _PyObject_GetManagedDict(obj);
7488+
if (dict != NULL && ((PyDictObject *)dict)->ma_values != values) {
7489+
int res;
7490+
Py_BEGIN_CRITICAL_SECTION(dict);
7491+
res = _PyDict_SetItem_LockHeld(dict, name, value);
7492+
Py_END_CRITICAL_SECTION();
7493+
return res;
74897494
}
7490-
#endif
74917495
}
7496+
#else
7497+
ix = insert_split_key(keys, name, hash);
7498+
#endif
74927499

74937500
if (ix == DKIX_EMPTY) {
7494-
int res;
7495-
if (dict == NULL) {
7496-
// Make the dict but don't publish it in the object
7497-
// so that no one else will see it.
7498-
dict = make_dict_from_instance_attributes(keys, values);
7499-
if (dict == NULL ||
7500-
_PyDict_SetItem_LockHeld(dict, name, value) < 0) {
7501-
Py_XDECREF(dict);
7502-
return -1;
7503-
}
7501+
#ifdef Py_STATS
7502+
if (shared_keys_usable_size(keys) == SHARED_KEYS_MAX_SIZE) {
7503+
OBJECT_STAT_INC(dict_materialized_too_big);
7504+
}
7505+
else {
7506+
OBJECT_STAT_INC(dict_materialized_new_key);
7507+
}
7508+
#endif
75047509

7505-
FT_ATOMIC_STORE_PTR_RELEASE(_PyObject_ManagedDictPointer(obj)->dict,
7506-
(PyDictObject *)dict);
7507-
return 0;
7510+
// Make the dict but don't publish it in the object
7511+
// so that no one else will see it.
7512+
PyDictObject *dict = make_dict_from_instance_attributes(keys, values);
7513+
if (dict == NULL ||
7514+
_PyDict_SetItem_LockHeld(dict, name, value) < 0) {
7515+
Py_XDECREF(dict);
7516+
return -1;
75087517
}
75097518

7510-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(dict);
7519+
FT_ATOMIC_STORE_PTR_RELEASE(_PyObject_ManagedDictPointer(obj)->dict,
7520+
(PyDictObject *)dict);
7521+
return 0;
75117522

7512-
res = _PyDict_SetItem_LockHeld(dict, name, value);
7513-
return res;
75147523
}
75157524

75167525
PyObject *old_value = values->values[ix];
@@ -7522,107 +7531,64 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values,
75227531
return -1;
75237532
}
75247533

7525-
if (dict) {
7526-
PyDict_WatchEvent event = (old_value == NULL ? PyDict_EVENT_ADDED :
7527-
value == NULL ? PyDict_EVENT_DELETED :
7528-
PyDict_EVENT_MODIFIED);
7529-
_PyDict_NotifyEvent(event, dict, name, value);
7530-
}
7531-
75327534
FT_ATOMIC_STORE_PTR_RELEASE(values->values[ix], Py_XNewRef(value));
75337535

75347536
if (old_value == NULL) {
75357537
_PyDictValues_AddToInsertionOrder(values, ix);
7536-
if (dict) {
7537-
assert(dict->ma_values == values);
7538-
STORE_USED(dict, dict->ma_used + 1);
7539-
}
75407538
}
75417539
else {
75427540
if (value == NULL) {
75437541
delete_index_from_values(values, ix);
7544-
if (dict) {
7545-
assert(dict->ma_values == values);
7546-
STORE_USED(dict, dict->ma_used - 1);
7547-
}
75487542
}
75497543
Py_DECREF(old_value);
75507544
}
75517545
return 0;
75527546
}
75537547

7554-
static inline int
7555-
store_instance_attr_dict(PyObject *obj, PyDictObject *dict, PyObject *name, PyObject *value)
7556-
{
7557-
PyDictValues *values = _PyObject_InlineValues(obj);
7558-
int res;
7559-
Py_BEGIN_CRITICAL_SECTION(dict);
7560-
if (dict->ma_values == values) {
7561-
res = store_instance_attr_lock_held(obj, values, name, value);
7562-
}
7563-
else {
7564-
res = _PyDict_SetItem_LockHeld(dict, name, value);
7565-
}
7566-
Py_END_CRITICAL_SECTION();
7567-
return res;
7568-
}
7569-
75707548
int
75717549
_PyObject_StoreInstanceAttribute(PyObject *obj, PyObject *name, PyObject *value)
75727550
{
75737551
PyDictValues *values = _PyObject_InlineValues(obj);
7574-
if (!FT_ATOMIC_LOAD_UINT8(values->valid)) {
7575-
PyDictObject *dict;
7576-
int res;
7577-
#ifdef Py_GIL_DISABLED
7578-
Py_BEGIN_CRITICAL_SECTION(obj);
7579-
#endif
7580-
dict = _PyObject_GetManagedDict(obj);
7581-
Py_XINCREF(dict);
7582-
#ifdef Py_GIL_DISABLED
7583-
Py_END_CRITICAL_SECTION();
7584-
#endif
7552+
7553+
int res;
7554+
PyDictObject *dict;
7555+
Py_BEGIN_CRITICAL_SECTION(obj);
7556+
dict = _PyObject_GetManagedDict(obj);
7557+
7558+
if (!FT_ATOMIC_LOAD_UINT8(values->valid) || !PyUnicode_CheckExact(name)) {
75857559
if (dict == NULL) {
7560+
#ifdef Py_STATS
7561+
if (!PyUnicode_CheckExact(name)) {
7562+
OBJECT_STAT_INC(dict_materialized_str_subclass);
7563+
}
7564+
#endif
75867565
dict = (PyDictObject *)PyObject_GenericGetDict(obj, NULL);
75877566
if (dict == NULL) {
7588-
return -1;
7567+
res = -1;
75897568
}
75907569
}
7591-
res = store_instance_attr_dict(obj, dict, name, value);
7592-
Py_DECREF(dict);
7593-
return res;
7570+
else {
7571+
Py_INCREF(dict);
7572+
}
7573+
}
7574+
else if (dict == NULL) {
7575+
res = store_instance_attr_lock_held(obj, values, name, value);
7576+
}
7577+
else {
7578+
Py_INCREF(dict);
75947579
}
75957580

7596-
#ifdef Py_GIL_DISABLED
7597-
// We have a valid inline values, at least for now... There are two potential
7598-
// races with having the values become invalid. One is the dictionary
7599-
// being detached from the object. The other is if someone is inserting
7600-
// into the dictionary directly and therefore causing it to resize.
7601-
//
7602-
// If we haven't materialized the dictionary yet we lock on the object, which
7603-
// will also be used to prevent the dictionary from being materialized while
7604-
// we're doing the insertion. If we race and the dictionary gets created
7605-
// then we'll need to release the object lock and lock the dictionary to
7606-
// prevent resizing.
7607-
PyDictObject *dict = _PyObject_GetManagedDict(obj);
7608-
if (dict == NULL) {
7609-
int res;
7610-
Py_BEGIN_CRITICAL_SECTION(obj);
7611-
dict = _PyObject_GetManagedDict(obj);
7612-
7613-
if (dict == NULL) {
7614-
res = store_instance_attr_lock_held(obj, values, name, value);
7615-
}
7616-
Py_END_CRITICAL_SECTION();
7581+
Py_END_CRITICAL_SECTION();
76177582

7618-
if (dict == NULL) {
7619-
return res;
7620-
}
7583+
if (dict == NULL) {
7584+
return res;
76217585
}
7622-
return store_instance_attr_dict(obj, dict, name, value);
7623-
#else
7624-
return store_instance_attr_lock_held(obj, values, name, value);
7625-
#endif
7586+
7587+
Py_BEGIN_CRITICAL_SECTION(dict);
7588+
res = _PyDict_SetItem_LockHeld(dict, name, value);
7589+
Py_END_CRITICAL_SECTION();
7590+
Py_DECREF(dict);
7591+
return res;
76267592
}
76277593

76287594
/* Sanity check for managed dicts */
@@ -7868,8 +7834,9 @@ replace_dict_probably_inline_materialized(PyObject *obj, PyDictObject *inline_di
78687834
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
78697835

78707836
if (cur_dict == inline_dict) {
7871-
assert(FT_ATOMIC_LOAD_PTR_RELAXED(inline_dict->ma_values) == _PyObject_InlineValues(obj));
7872-
7837+
// The dict may no longer point at the inline values if it was
7838+
// resized while we didn't hold its lock, in which case detaching
7839+
// is a no-op.
78737840
int err = _PyDict_DetachFromObject(inline_dict, obj);
78747841
if (err != 0) {
78757842
assert(new_dict == NULL);
@@ -7895,14 +7862,46 @@ decref_maybe_delay(PyObject *obj, bool delay)
78957862
}
78967863
}
78977864

7865+
#if !defined(NDEBUG) && defined(Py_GIL_DISABLED)
7866+
// A materialized dict can be resized while only its own lock is held, so
7867+
// the inline values are only guaranteed to be consistent once both the
7868+
// object and the dict are locked.
7869+
static int
7870+
inline_values_consistency_check_locked(PyObject *obj)
7871+
{
7872+
PyDictObject *dict;
7873+
int res = 1;
7874+
7875+
Py_BEGIN_CRITICAL_SECTION(obj);
7876+
dict = _PyObject_GetManagedDict(obj);
7877+
Py_XINCREF(dict);
7878+
Py_END_CRITICAL_SECTION();
7879+
7880+
if (dict == NULL) {
7881+
return 1;
7882+
}
7883+
7884+
Py_BEGIN_CRITICAL_SECTION2(obj, dict);
7885+
if (_PyObject_GetManagedDict(obj) == dict) {
7886+
res = _PyObject_InlineValuesConsistencyCheck(obj);
7887+
}
7888+
Py_END_CRITICAL_SECTION2();
7889+
7890+
decref_maybe_delay((PyObject *)dict, true);
7891+
return res;
7892+
}
7893+
#endif
7894+
78987895
int
78997896
_PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict)
79007897
{
79017898
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT);
79027899
#ifndef NDEBUG
7903-
Py_BEGIN_CRITICAL_SECTION(obj);
7900+
#ifdef Py_GIL_DISABLED
7901+
assert(inline_values_consistency_check_locked(obj));
7902+
#else
79047903
assert(_PyObject_InlineValuesConsistencyCheck(obj));
7905-
Py_END_CRITICAL_SECTION();
7904+
#endif
79067905
#endif
79077906
int err = 0;
79087907
PyTypeObject *tp = Py_TYPE(obj);

0 commit comments

Comments
 (0)