-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
gh-124470: Fix crash when reading from object instance dictionary while replacing it #122489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7087,51 +7087,137 @@ set_dict_inline_values(PyObject *obj, PyDictObject *new_dict) | |
} | ||
} | ||
|
||
int | ||
_PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict) | ||
#ifdef Py_GIL_DISABLED | ||
|
||
// Trys and sets the dictionary for an object in the easy case when our current | ||
// dictionary is either completely not materialized or is a dictionary which | ||
// does not point at the inline values. | ||
static bool | ||
try_set_dict_inline_only_or_other_dict(PyObject *obj, PyObject *new_dict, PyDictObject **cur_dict) | ||
{ | ||
bool replaced = false; | ||
Py_BEGIN_CRITICAL_SECTION(obj); | ||
|
||
PyDictObject *dict = *cur_dict = _PyObject_GetManagedDict(obj); | ||
if (dict == NULL) { | ||
// We only have inline values, we can just completely replace them. | ||
set_dict_inline_values(obj, (PyDictObject *)new_dict); | ||
replaced = true; | ||
goto exit_lock; | ||
} | ||
|
||
if (FT_ATOMIC_LOAD_PTR_RELAXED(dict->ma_values) != _PyObject_InlineValues(obj)) { | ||
// We have a materialized dict which doesn't point at the inline values, | ||
// We get to simply swap dictionaries and free the old dictionary. | ||
FT_ATOMIC_STORE_PTR(_PyObject_ManagedDictPointer(obj)->dict, | ||
(PyDictObject *)Py_XNewRef(new_dict)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe align the wrapped line |
||
replaced = true; | ||
goto exit_lock; | ||
} else { | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// We have inline values, we need to lock the dict and the object | ||
// at the same time to safely dematerialize them. To do that while releasing | ||
// the object lock we need a strong reference to the current dictionary. | ||
Py_INCREF(dict); | ||
} | ||
exit_lock: | ||
Py_END_CRITICAL_SECTION(); | ||
return replaced; | ||
} | ||
|
||
// Replaces a dictionary that is probably the dictionary which has been | ||
// materialized and points at the inline values. We could have raced | ||
// and replaced it with another dictionary though. | ||
static int | ||
replace_dict_probably_inline_materialized(PyObject *obj, PyDictObject *inline_dict, | ||
PyObject *new_dict, bool clear, | ||
PyDictObject **replaced_dict) | ||
{ | ||
// But we could have had another thread race in after we released | ||
// the object lock | ||
int err = 0; | ||
*replaced_dict = _PyObject_GetManagedDict(obj); | ||
assert(FT_ATOMIC_LOAD_PTR_RELAXED(inline_dict->ma_values) == _PyObject_InlineValues(obj)); | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (*replaced_dict == inline_dict) { | ||
err = _PyDict_DetachFromObject(inline_dict, obj); | ||
if (err != 0) { | ||
assert(new_dict == NULL); | ||
return err; | ||
} | ||
// We incref'd the inline dict and the object owns a ref. | ||
// Clear the object's reference, we'll clear the local | ||
// reference after releasing the lock. | ||
if (clear) { | ||
Py_XDECREF((PyObject *)*replaced_dict); | ||
} else { | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_PyObject_XDecRefDelayed((PyObject *)*replaced_dict); | ||
} | ||
*replaced_dict = NULL; | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
FT_ATOMIC_STORE_PTR(_PyObject_ManagedDictPointer(obj)->dict, | ||
(PyDictObject *)Py_XNewRef(new_dict)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: align |
||
return err; | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
#endif | ||
|
||
static int | ||
set_or_clear_managed_dict(PyObject *obj, PyObject *new_dict, bool clear) | ||
{ | ||
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT); | ||
assert(_PyObject_InlineValuesConsistencyCheck(obj)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these consistency checks need to be within some sort of lock? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type flag one is fine, I'll add some ifdefs and lock for the inline check. |
||
int err = 0; | ||
PyTypeObject *tp = Py_TYPE(obj); | ||
if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) { | ||
PyDictObject *dict = _PyObject_GetManagedDict(obj); | ||
if (dict == NULL) { | ||
#ifdef Py_GIL_DISABLED | ||
Py_BEGIN_CRITICAL_SECTION(obj); | ||
PyDictObject *prev_dict; | ||
if (!try_set_dict_inline_only_or_other_dict(obj, new_dict, &prev_dict)) { | ||
// We had a materialized dictionary which pointed at the inline | ||
// values. We need to lock both the object and the dict at the | ||
// same time to safely replace it. We can't merely lock the dictionary | ||
// while the object is locked because it could suspend the object lock. | ||
PyDictObject *replaced_dict; | ||
|
||
dict = _PyObject_ManagedDictPointer(obj)->dict; | ||
if (dict == NULL) { | ||
set_dict_inline_values(obj, (PyDictObject *)new_dict); | ||
} | ||
assert(prev_dict != NULL); | ||
Py_BEGIN_CRITICAL_SECTION2(obj, prev_dict); | ||
|
||
Py_END_CRITICAL_SECTION(); | ||
err = replace_dict_probably_inline_materialized(obj, prev_dict, new_dict, | ||
clear, &replaced_dict); | ||
|
||
if (dict == NULL) { | ||
return 0; | ||
Py_END_CRITICAL_SECTION2(); | ||
|
||
Py_DECREF(prev_dict); | ||
if (err != 0) { | ||
return err; | ||
} | ||
prev_dict = replaced_dict; | ||
} | ||
|
||
if (prev_dict != NULL) { | ||
// Readers from the old dictionary use a borrowed reference. We need | ||
// to set the decref the dict at the next safe point. | ||
if (clear) { | ||
Py_XDECREF((PyObject *)prev_dict); | ||
} else { | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_PyObject_XDecRefDelayed((PyObject *)prev_dict); | ||
} | ||
} | ||
return 0; | ||
#else | ||
PyDictObject *dict = _PyObject_GetManagedDict(obj); | ||
if (dict == NULL) { | ||
set_dict_inline_values(obj, (PyDictObject *)new_dict); | ||
return 0; | ||
#endif | ||
} | ||
|
||
Py_BEGIN_CRITICAL_SECTION2(dict, obj); | ||
|
||
// We've locked dict, but the actual dict could have changed | ||
// since we locked it. | ||
dict = _PyObject_ManagedDictPointer(obj)->dict; | ||
err = _PyDict_DetachFromObject(dict, obj); | ||
assert(err == 0 || new_dict == NULL); | ||
if (err == 0) { | ||
FT_ATOMIC_STORE_PTR(_PyObject_ManagedDictPointer(obj)->dict, | ||
(PyDictObject *)Py_XNewRef(new_dict)); | ||
} | ||
Py_END_CRITICAL_SECTION2(); | ||
|
||
if (err == 0) { | ||
Py_XDECREF(dict); | ||
if (_PyDict_DetachFromObject(dict, obj) == 0) { | ||
_PyObject_ManagedDictPointer(obj)->dict = (PyDictObject *)Py_XNewRef(new_dict); | ||
Py_DECREF(dict); | ||
return 0; | ||
} | ||
assert(new_dict == NULL); | ||
return -1; | ||
#endif | ||
} | ||
else { | ||
PyDictObject *dict; | ||
|
@@ -7144,17 +7230,26 @@ _PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict) | |
(PyDictObject *)Py_XNewRef(new_dict)); | ||
|
||
Py_END_CRITICAL_SECTION(); | ||
|
||
Py_XDECREF(dict); | ||
if (clear) { | ||
Py_XDECREF((PyObject *)dict); | ||
} else { | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_PyObject_XDecRefDelayed((PyObject *)dict); | ||
} | ||
} | ||
assert(_PyObject_InlineValuesConsistencyCheck(obj)); | ||
return err; | ||
} | ||
|
||
int | ||
_PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict) | ||
{ | ||
return set_or_clear_managed_dict(obj, new_dict, false); | ||
} | ||
|
||
void | ||
PyObject_ClearManagedDict(PyObject *obj) | ||
{ | ||
if (_PyObject_SetManagedDict(obj, NULL) < 0) { | ||
if (set_or_clear_managed_dict(obj, NULL, true) < 0) { | ||
/* Must be out of memory */ | ||
assert(PyErr_Occurred() == PyExc_MemoryError); | ||
PyErr_WriteUnraisable(NULL); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stray whitespace change