Skip to content

Commit 72a0e10

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

2 files changed

Lines changed: 165 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: 115 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,57 @@ 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(PyUnicode_CheckExact(name));
74737473

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

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-
}
7480+
#ifdef Py_GIL_DISABLED
7481+
ix = unicodekeys_lookup_unicode_threadsafe(keys, name, hash);
7482+
if (ix < 0) {
7483+
// insert split key locks which can release our object lock opening
7484+
// up an opportunity to create the dict.
7485+
ix = insert_split_key(keys, name, hash);
7486+
PyDictObject *dict = _PyObject_GetManagedDict(obj);
7487+
if (dict != NULL && ((PyDictObject *)dict)->ma_values != values) {
7488+
int res;
7489+
Py_BEGIN_CRITICAL_SECTION(dict);
7490+
res = _PyDict_SetItem_LockHeld(dict, name, value);
7491+
Py_END_CRITICAL_SECTION();
7492+
return res;
74897493
}
7490-
#endif
74917494
}
7495+
#else
7496+
ix = insert_split_key(keys, name, hash);
7497+
#endif
74927498

74937499
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-
}
7500+
#ifdef Py_STATS
7501+
if (shared_keys_usable_size(keys) == SHARED_KEYS_MAX_SIZE) {
7502+
OBJECT_STAT_INC(dict_materialized_too_big);
7503+
}
7504+
else {
7505+
OBJECT_STAT_INC(dict_materialized_new_key);
7506+
}
7507+
#endif
75047508

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

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

7512-
res = _PyDict_SetItem_LockHeld(dict, name, value);
7513-
return res;
75147522
}
75157523

75167524
PyObject *old_value = values->values[ix];
@@ -7522,107 +7530,64 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values,
75227530
return -1;
75237531
}
75247532

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-
75327533
FT_ATOMIC_STORE_PTR_RELEASE(values->values[ix], Py_XNewRef(value));
75337534

75347535
if (old_value == NULL) {
75357536
_PyDictValues_AddToInsertionOrder(values, ix);
7536-
if (dict) {
7537-
assert(dict->ma_values == values);
7538-
STORE_USED(dict, dict->ma_used + 1);
7539-
}
75407537
}
75417538
else {
75427539
if (value == NULL) {
75437540
delete_index_from_values(values, ix);
7544-
if (dict) {
7545-
assert(dict->ma_values == values);
7546-
STORE_USED(dict, dict->ma_used - 1);
7547-
}
75487541
}
75497542
Py_DECREF(old_value);
75507543
}
75517544
return 0;
75527545
}
75537546

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-
75707547
int
75717548
_PyObject_StoreInstanceAttribute(PyObject *obj, PyObject *name, PyObject *value)
75727549
{
75737550
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
7551+
7552+
int res;
7553+
PyDictObject *dict;
7554+
Py_BEGIN_CRITICAL_SECTION(obj);
7555+
dict = _PyObject_GetManagedDict(obj);
7556+
7557+
if (!FT_ATOMIC_LOAD_UINT8(values->valid) || !PyUnicode_CheckExact(name)) {
75857558
if (dict == NULL) {
7559+
#ifdef Py_STATS
7560+
if (!PyUnicode_CheckExact(name)) {
7561+
OBJECT_STAT_INC(dict_materialized_str_subclass);
7562+
}
7563+
#endif
75867564
dict = (PyDictObject *)PyObject_GenericGetDict(obj, NULL);
75877565
if (dict == NULL) {
7588-
return -1;
7566+
res = -1;
75897567
}
75907568
}
7591-
res = store_instance_attr_dict(obj, dict, name, value);
7592-
Py_DECREF(dict);
7593-
return res;
7569+
else {
7570+
Py_INCREF(dict);
7571+
}
7572+
}
7573+
else if (dict == NULL) {
7574+
res = store_instance_attr_lock_held(obj, values, name, value);
7575+
}
7576+
else {
7577+
Py_INCREF(dict);
75947578
}
75957579

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();
7580+
Py_END_CRITICAL_SECTION();
76177581

7618-
if (dict == NULL) {
7619-
return res;
7620-
}
7582+
if (dict == NULL) {
7583+
return res;
76217584
}
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
7585+
7586+
Py_BEGIN_CRITICAL_SECTION(dict);
7587+
res = _PyDict_SetItem_LockHeld(dict, name, value);
7588+
Py_END_CRITICAL_SECTION();
7589+
Py_DECREF(dict);
7590+
return res;
76267591
}
76277592

76287593
/* Sanity check for managed dicts */
@@ -7868,8 +7833,9 @@ replace_dict_probably_inline_materialized(PyObject *obj, PyDictObject *inline_di
78687833
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
78697834

78707835
if (cur_dict == inline_dict) {
7871-
assert(FT_ATOMIC_LOAD_PTR_RELAXED(inline_dict->ma_values) == _PyObject_InlineValues(obj));
7872-
7836+
// The dict may no longer point at the inline values if it was
7837+
// resized while we didn't hold its lock, in which case detaching
7838+
// is a no-op.
78737839
int err = _PyDict_DetachFromObject(inline_dict, obj);
78747840
if (err != 0) {
78757841
assert(new_dict == NULL);
@@ -7895,14 +7861,46 @@ decref_maybe_delay(PyObject *obj, bool delay)
78957861
}
78967862
}
78977863

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

0 commit comments

Comments
 (0)