Skip to content

Commit 69bb1c6

Browse files
authored
[3.13] gh-127582: Make object resurrection thread-safe for free threading. (GH-127612) (GH-127659)
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. (cherry picked from commit f4f5308)
1 parent 304111e commit 69bb1c6

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
@@ -572,8 +572,52 @@ _PyObject_SetMaybeWeakref(PyObject *op)
572572
}
573573
}
574574

575+
extern int _PyObject_ResurrectEndSlow(PyObject *op);
575576
#endif
576577

578+
// Temporarily resurrects an object during deallocation. The refcount is set
579+
// to one.
580+
static inline void
581+
_PyObject_ResurrectStart(PyObject *op)
582+
{
583+
assert(Py_REFCNT(op) == 0);
584+
#ifdef Py_REF_DEBUG
585+
_Py_IncRefTotal(_PyThreadState_GET());
586+
#endif
587+
#ifdef Py_GIL_DISABLED
588+
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_ThreadId());
589+
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 1);
590+
_Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, 0);
591+
#else
592+
Py_SET_REFCNT(op, 1);
593+
#endif
594+
}
595+
596+
// Undoes an object resurrection by decrementing the refcount without calling
597+
// _Py_Dealloc(). Returns 0 if the object is dead (the normal case), and
598+
// deallocation should continue. Returns 1 if the object is still alive.
599+
static inline int
600+
_PyObject_ResurrectEnd(PyObject *op)
601+
{
602+
#ifdef Py_REF_DEBUG
603+
_Py_DecRefTotal(_PyThreadState_GET());
604+
#endif
605+
#ifndef Py_GIL_DISABLED
606+
Py_SET_REFCNT(op, Py_REFCNT(op) - 1);
607+
return Py_REFCNT(op) != 0;
608+
#else
609+
uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local);
610+
Py_ssize_t shared = _Py_atomic_load_ssize_acquire(&op->ob_ref_shared);
611+
if (_Py_IsOwnedByCurrentThread(op) && local == 1 && shared == 0) {
612+
// Fast-path: object has a single refcount and is owned by this thread
613+
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0);
614+
return 0;
615+
}
616+
// Slow-path: object has a shared refcount or is not owned by this thread
617+
return _PyObject_ResurrectEndSlow(op);
618+
#endif
619+
}
620+
577621
/* Tries to incref op and returns 1 if successful or 0 otherwise. */
578622
static inline int
579623
_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
@@ -1845,14 +1845,11 @@ free_monitoring_data(_PyCoMonitoringData *data)
18451845
static void
18461846
code_dealloc(PyCodeObject *co)
18471847
{
1848-
assert(Py_REFCNT(co) == 0);
1849-
Py_SET_REFCNT(co, 1);
1848+
_PyObject_ResurrectStart((PyObject *)co);
18501849
notify_code_watchers(PY_CODE_EVENT_DESTROY, co);
1851-
if (Py_REFCNT(co) > 1) {
1852-
Py_SET_REFCNT(co, Py_REFCNT(co) - 1);
1850+
if (_PyObject_ResurrectEnd((PyObject *)co)) {
18531851
return;
18541852
}
1855-
Py_SET_REFCNT(co, 0);
18561853

18571854
#ifdef Py_GIL_DISABLED
18581855
PyObject_GC_UnTrack(co);

Objects/dictobject.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,14 +3150,11 @@ dict_dealloc(PyObject *self)
31503150
{
31513151
PyDictObject *mp = (PyDictObject *)self;
31523152
PyInterpreterState *interp = _PyInterpreterState_GET();
3153-
assert(Py_REFCNT(mp) == 0);
3154-
Py_SET_REFCNT(mp, 1);
3153+
_PyObject_ResurrectStart(self);
31553154
_PyDict_NotifyEvent(interp, PyDict_EVENT_DEALLOCATED, mp, NULL, NULL);
3156-
if (Py_REFCNT(mp) > 1) {
3157-
Py_SET_REFCNT(mp, Py_REFCNT(mp) - 1);
3155+
if (_PyObject_ResurrectEnd(self)) {
31583156
return;
31593157
}
3160-
Py_SET_REFCNT(mp, 0);
31613158
PyDictValues *values = mp->ma_values;
31623159
PyDictKeysObject *keys = mp->ma_keys;
31633160
Py_ssize_t i, n;

Objects/funcobject.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -986,14 +986,11 @@ func_clear(PyFunctionObject *op)
986986
static void
987987
func_dealloc(PyFunctionObject *op)
988988
{
989-
assert(Py_REFCNT(op) == 0);
990-
Py_SET_REFCNT(op, 1);
989+
_PyObject_ResurrectStart((PyObject *)op);
991990
handle_func_event(PyFunction_EVENT_DESTROY, op, NULL);
992-
if (Py_REFCNT(op) > 1) {
993-
Py_SET_REFCNT(op, Py_REFCNT(op) - 1);
991+
if (_PyObject_ResurrectEnd((PyObject *)op)) {
994992
return;
995993
}
996-
Py_SET_REFCNT(op, 0);
997994
_PyObject_GC_UNTRACK(op);
998995
if (op->func_weakreflist != NULL) {
999996
PyObject_ClearWeakRefs((PyObject *) op);

Objects/object.c

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

363-
void
364-
_Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
363+
// Decrement the shared reference count of an object. Return 1 if the object
364+
// is dead and should be deallocated, 0 otherwise.
365+
static int
366+
_Py_DecRefSharedIsDead(PyObject *o, const char *filename, int lineno)
365367
{
366368
// Should we queue the object for the owning thread to merge?
367369
int should_queue;
@@ -402,6 +404,15 @@ _Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
402404
}
403405
else if (new_shared == _Py_REF_MERGED) {
404406
// refcount is zero AND merged
407+
return 1;
408+
}
409+
return 0;
410+
}
411+
412+
void
413+
_Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
414+
{
415+
if (_Py_DecRefSharedIsDead(o, filename, lineno)) {
405416
_Py_Dealloc(o);
406417
}
407418
}
@@ -470,6 +481,26 @@ _Py_ExplicitMergeRefcount(PyObject *op, Py_ssize_t extra)
470481
&shared, new_shared));
471482
return refcnt;
472483
}
484+
485+
// The more complicated "slow" path for undoing the resurrection of an object.
486+
int
487+
_PyObject_ResurrectEndSlow(PyObject *op)
488+
{
489+
if (_Py_IsImmortal(op)) {
490+
return 1;
491+
}
492+
if (_Py_IsOwnedByCurrentThread(op)) {
493+
// If the object is owned by the current thread, give up ownership and
494+
// merge the refcount. This isn't necessary in all cases, but it
495+
// simplifies the implementation.
496+
Py_ssize_t refcount = _Py_ExplicitMergeRefcount(op, -1);
497+
return refcount != 0;
498+
}
499+
int is_dead = _Py_DecRefSharedIsDead(op, NULL, 0);
500+
return !is_dead;
501+
}
502+
503+
473504
#endif /* Py_GIL_DISABLED */
474505

475506

@@ -548,7 +579,7 @@ PyObject_CallFinalizerFromDealloc(PyObject *self)
548579
}
549580

550581
/* Temporarily resurrect the object. */
551-
Py_SET_REFCNT(self, 1);
582+
_PyObject_ResurrectStart(self);
552583

553584
PyObject_CallFinalizer(self);
554585

@@ -558,8 +589,7 @@ PyObject_CallFinalizerFromDealloc(PyObject *self)
558589

559590
/* Undo the temporary resurrection; can't use DECREF here, it would
560591
* cause a recursive call. */
561-
Py_SET_REFCNT(self, Py_REFCNT(self) - 1);
562-
if (Py_REFCNT(self) == 0) {
592+
if (!_PyObject_ResurrectEnd(self)) {
563593
return 0; /* this is the normal path out */
564594
}
565595

0 commit comments

Comments
 (0)