-
Notifications
You must be signed in to change notification settings - Fork 211
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
Fix race condition involving wrapper lookup #865
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,60 @@ static PyObject **nb_weaklist_ptr(PyObject *self) { | |
return weaklistoffset ? (PyObject **) ((uint8_t *) self + weaklistoffset) : nullptr; | ||
} | ||
|
||
static void nb_enable_try_inc_ref(PyObject *obj) noexcept { | ||
#if 0 && defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E00A5 | ||
PyUnstable_EnableTryIncRef(obj); | ||
#elif defined(Py_GIL_DISABLED) | ||
// Since this is called during object construction, we know that we have | ||
// the only reference to the object and can use a non-atomic write. | ||
assert(obj->ob_ref_shared == 0); | ||
obj->ob_ref_shared = _Py_REF_MAYBE_WEAKREF; | ||
#endif | ||
} | ||
|
||
static bool nb_try_inc_ref(PyObject *obj) noexcept { | ||
#if 0 && defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E00A5 | ||
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. Is the |
||
return PyUnstable_TryIncRef(obj); | ||
#elif defined(Py_GIL_DISABLED) | ||
// See https://github.com/python/cpython/blob/d05140f9f77d7dfc753dd1e5ac3a5962aaa03eff/Include/internal/pycore_object.h#L761 | ||
uint32_t local = _Py_atomic_load_uint32_relaxed(&obj->ob_ref_local); | ||
local += 1; | ||
if (local == 0) { | ||
// immortal | ||
return true; | ||
} | ||
if (_Py_IsOwnedByCurrentThread(obj)) { | ||
_Py_atomic_store_uint32_relaxed(&obj->ob_ref_local, local); | ||
#ifdef Py_REF_DEBUG | ||
_Py_INCREF_IncRefTotal(); | ||
#endif | ||
return true; | ||
} | ||
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&obj->ob_ref_shared); | ||
for (;;) { | ||
// If the shared refcount is zero and the object is either merged | ||
// or may not have weak references, then we cannot incref it. | ||
if (shared == 0 || shared == _Py_REF_MERGED) { | ||
return false; | ||
} | ||
|
||
if (_Py_atomic_compare_exchange_ssize( | ||
&obj->ob_ref_shared, &shared, shared + (1 << _Py_REF_SHARED_SHIFT))) { | ||
#ifdef Py_REF_DEBUG | ||
_Py_INCREF_IncRefTotal(); | ||
#endif | ||
return true; | ||
} | ||
} | ||
#else | ||
if (Py_REFCNT(obj) > 0) { | ||
Py_INCREF(obj); | ||
return true; | ||
} | ||
return false; | ||
#endif | ||
} | ||
|
||
static PyGetSetDef inst_getset[] = { | ||
{ "__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, nullptr, nullptr }, | ||
{ nullptr, nullptr, nullptr, nullptr, nullptr } | ||
|
@@ -98,6 +152,7 @@ PyObject *inst_new_int(PyTypeObject *tp, PyObject * /* args */, | |
self->clear_keep_alive = 0; | ||
self->intrusive = intrusive; | ||
self->unused = 0; | ||
nb_enable_try_inc_ref((PyObject *)self); | ||
|
||
// Update hash table that maps from C++ to Python instance | ||
nb_shard &shard = internals->shard((void *) payload); | ||
|
@@ -163,6 +218,7 @@ PyObject *inst_new_ext(PyTypeObject *tp, void *value) { | |
self->clear_keep_alive = 0; | ||
self->intrusive = intrusive; | ||
self->unused = 0; | ||
nb_enable_try_inc_ref((PyObject *)self); | ||
|
||
nb_shard &shard = internals->shard(value); | ||
lock_shard guard(shard); | ||
|
@@ -1766,16 +1822,18 @@ PyObject *nb_type_put(const std::type_info *cpp_type, | |
PyTypeObject *tp = Py_TYPE(seq.inst); | ||
|
||
if (nb_type_data(tp)->type == cpp_type) { | ||
Py_INCREF(seq.inst); | ||
return seq.inst; | ||
if (nb_try_inc_ref(seq.inst)) { | ||
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. I think that if Ditto below. There are also two other places, where this 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. I think breaking out here would create more wrappers than necessary. The case I'm thinking about is where there are multiple lookups before the to-be-deallocated wrapper is removed from T1: starts to deallocate wrapper object, but hasn't removed it from 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. In nanobind (and pybind11, for that matter), a single pointer can be associated with multiple Python instances. For example, you could have
with both an The Your commit adds a failure mechanism, where it turns out that one wrapper is in the process of being deleted, and we need to create a new one. In this case, it (AFAIK) doesn't make sense to try the other types associated with the current pointer, since we already found the one. Hence the suggestion to 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. Hum, thinking more about this, I think that is currently necessary continue iterating. There could be multiple objects that are in the process of being cleaned up. 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. You can also get multiple nanobind instances wrapping the same pointer, with the same type, by using |
||
return seq.inst; | ||
} | ||
} | ||
|
||
if (!lookup_type()) | ||
return nullptr; | ||
|
||
if (PyType_IsSubtype(tp, td->type_py)) { | ||
Py_INCREF(seq.inst); | ||
return seq.inst; | ||
if (nb_try_inc_ref(seq.inst)) { | ||
return seq.inst; | ||
} | ||
} | ||
|
||
if (seq.next == nullptr) | ||
|
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.
Is the 0 intentional here?
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.
The APIs haven't been added to CPython yet, but I wanted to get this PR ready while the issue was still fresh in my mind. We can either:
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.
I think it's OK to commit the code as is, we can remove the
#if 0
part once the change to CPython goes in.Do I understand correctly that the version below now has an optimization (specific to object construction) that won't be in the official API?
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.
I intend to add a similar optimization in the implementation of the CPython API.