Skip to content

Commit f4f5308

Browse files
authored
gh-127582: Make object resurrection thread-safe for free threading. (GH-127612)
Objects may be temporarily "resurrected" in destructors when calling finalizers or watcher callbacks. We previously undid the resurrection by decrementing the reference count using `Py_SET_REFCNT`. This was not thread-safe because other threads might be accessing the object (modifying its reference count) if it was exposed by the finalizer, watcher callback, or temporarily accessed by a racy dictionary or list access. This adds internal-only thread-safe functions for temporary object resurrection during destructors.
1 parent 657d0e9 commit f4f5308

File tree

6 files changed

+87
-20
lines changed

6 files changed

+87
-20
lines changed

Include/internal/pycore_object.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,52 @@ _PyObject_SetMaybeWeakref(PyObject *op)
697697
}
698698
}
699699

700+
extern int _PyObject_ResurrectEndSlow(PyObject *op);
700701
#endif
701702

703+
// Temporarily resurrects an object during deallocation. The refcount is set
704+
// to one.
705+
static inline void
706+
_PyObject_ResurrectStart(PyObject *op)
707+
{
708+
assert(Py_REFCNT(op) == 0);
709+
#ifdef Py_REF_DEBUG
710+
_Py_IncRefTotal(_PyThreadState_GET());
711+
#endif
712+
#ifdef Py_GIL_DISABLED
713+
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_ThreadId());
714+
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 1);
715+
_Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, 0);
716+
#else
717+
Py_SET_REFCNT(op, 1);
718+
#endif
719+
}
720+
721+
// Undoes an object resurrection by decrementing the refcount without calling
722+
// _Py_Dealloc(). Returns 0 if the object is dead (the normal case), and
723+
// deallocation should continue. Returns 1 if the object is still alive.
724+
static inline int
725+
_PyObject_ResurrectEnd(PyObject *op)
726+
{
727+
#ifdef Py_REF_DEBUG
728+
_Py_DecRefTotal(_PyThreadState_GET());
729+
#endif
730+
#ifndef Py_GIL_DISABLED
731+
Py_SET_REFCNT(op, Py_REFCNT(op) - 1);
732+
return Py_REFCNT(op) != 0;
733+
#else
734+
uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local);
735+
Py_ssize_t shared = _Py_atomic_load_ssize_acquire(&op->ob_ref_shared);
736+
if (_Py_IsOwnedByCurrentThread(op) && local == 1 && shared == 0) {
737+
// Fast-path: object has a single refcount and is owned by this thread
738+
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0);
739+
return 0;
740+
}
741+
// Slow-path: object has a shared refcount or is not owned by this thread
742+
return _PyObject_ResurrectEndSlow(op);
743+
#endif
744+
}
745+
702746
/* Tries to incref op and returns 1 if successful or 0 otherwise. */
703747
static inline int
704748
_Py_TryIncref(PyObject *op)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix non-thread-safe object resurrection when calling finalizers and watcher
2+
callbacks in the free threading build.

Objects/codeobject.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,14 +1867,11 @@ free_monitoring_data(_PyCoMonitoringData *data)
18671867
static void
18681868
code_dealloc(PyCodeObject *co)
18691869
{
1870-
assert(Py_REFCNT(co) == 0);
1871-
Py_SET_REFCNT(co, 1);
1870+
_PyObject_ResurrectStart((PyObject *)co);
18721871
notify_code_watchers(PY_CODE_EVENT_DESTROY, co);
1873-
if (Py_REFCNT(co) > 1) {
1874-
Py_SET_REFCNT(co, Py_REFCNT(co) - 1);
1872+
if (_PyObject_ResurrectEnd((PyObject *)co)) {
18751873
return;
18761874
}
1877-
Py_SET_REFCNT(co, 0);
18781875

18791876
#ifdef Py_GIL_DISABLED
18801877
PyObject_GC_UnTrack(co);

Objects/dictobject.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3162,14 +3162,11 @@ dict_dealloc(PyObject *self)
31623162
{
31633163
PyDictObject *mp = (PyDictObject *)self;
31643164
PyInterpreterState *interp = _PyInterpreterState_GET();
3165-
assert(Py_REFCNT(mp) == 0);
3166-
Py_SET_REFCNT(mp, 1);
3165+
_PyObject_ResurrectStart(self);
31673166
_PyDict_NotifyEvent(interp, PyDict_EVENT_DEALLOCATED, mp, NULL, NULL);
3168-
if (Py_REFCNT(mp) > 1) {
3169-
Py_SET_REFCNT(mp, Py_REFCNT(mp) - 1);
3167+
if (_PyObject_ResurrectEnd(self)) {
31703168
return;
31713169
}
3172-
Py_SET_REFCNT(mp, 0);
31733170
PyDictValues *values = mp->ma_values;
31743171
PyDictKeysObject *keys = mp->ma_keys;
31753172
Py_ssize_t i, n;

Objects/funcobject.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,14 +1092,11 @@ static void
10921092
func_dealloc(PyObject *self)
10931093
{
10941094
PyFunctionObject *op = _PyFunction_CAST(self);
1095-
assert(Py_REFCNT(op) == 0);
1096-
Py_SET_REFCNT(op, 1);
1095+
_PyObject_ResurrectStart(self);
10971096
handle_func_event(PyFunction_EVENT_DESTROY, op, NULL);
1098-
if (Py_REFCNT(op) > 1) {
1099-
Py_SET_REFCNT(op, Py_REFCNT(op) - 1);
1097+
if (_PyObject_ResurrectEnd(self)) {
11001098
return;
11011099
}
1102-
Py_SET_REFCNT(op, 0);
11031100
_PyObject_GC_UNTRACK(op);
11041101
if (op->func_weakreflist != NULL) {
11051102
PyObject_ClearWeakRefs((PyObject *) op);

Objects/object.c

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,10 @@ is_dead(PyObject *o)
362362
}
363363
# endif
364364

365-
void
366-
_Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
365+
// Decrement the shared reference count of an object. Return 1 if the object
366+
// is dead and should be deallocated, 0 otherwise.
367+
static int
368+
_Py_DecRefSharedIsDead(PyObject *o, const char *filename, int lineno)
367369
{
368370
// Should we queue the object for the owning thread to merge?
369371
int should_queue;
@@ -404,6 +406,15 @@ _Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
404406
}
405407
else if (new_shared == _Py_REF_MERGED) {
406408
// refcount is zero AND merged
409+
return 1;
410+
}
411+
return 0;
412+
}
413+
414+
void
415+
_Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
416+
{
417+
if (_Py_DecRefSharedIsDead(o, filename, lineno)) {
407418
_Py_Dealloc(o);
408419
}
409420
}
@@ -472,6 +483,26 @@ _Py_ExplicitMergeRefcount(PyObject *op, Py_ssize_t extra)
472483
&shared, new_shared));
473484
return refcnt;
474485
}
486+
487+
// The more complicated "slow" path for undoing the resurrection of an object.
488+
int
489+
_PyObject_ResurrectEndSlow(PyObject *op)
490+
{
491+
if (_Py_IsImmortal(op)) {
492+
return 1;
493+
}
494+
if (_Py_IsOwnedByCurrentThread(op)) {
495+
// If the object is owned by the current thread, give up ownership and
496+
// merge the refcount. This isn't necessary in all cases, but it
497+
// simplifies the implementation.
498+
Py_ssize_t refcount = _Py_ExplicitMergeRefcount(op, -1);
499+
return refcount != 0;
500+
}
501+
int is_dead = _Py_DecRefSharedIsDead(op, NULL, 0);
502+
return !is_dead;
503+
}
504+
505+
475506
#endif /* Py_GIL_DISABLED */
476507

477508

@@ -550,7 +581,7 @@ PyObject_CallFinalizerFromDealloc(PyObject *self)
550581
}
551582

552583
/* Temporarily resurrect the object. */
553-
Py_SET_REFCNT(self, 1);
584+
_PyObject_ResurrectStart(self);
554585

555586
PyObject_CallFinalizer(self);
556587

@@ -560,8 +591,7 @@ PyObject_CallFinalizerFromDealloc(PyObject *self)
560591

561592
/* Undo the temporary resurrection; can't use DECREF here, it would
562593
* cause a recursive call. */
563-
Py_SET_REFCNT(self, Py_REFCNT(self) - 1);
564-
if (Py_REFCNT(self) == 0) {
594+
if (!_PyObject_ResurrectEnd(self)) {
565595
return 0; /* this is the normal path out */
566596
}
567597

0 commit comments

Comments
 (0)