summaryrefslogtreecommitdiffstats
path: root/Include
diff options
context:
space:
mode:
authorSam Gross <colesbury@gmail.com>2024-12-05 21:07:31 (GMT)
committerGitHub <noreply@github.com>2024-12-05 21:07:31 (GMT)
commitf4f530804b9d8f089eba0f157ec2144c03b13651 (patch)
treece40d723b8c991d786924c16dd04dfa96ff8689b /Include
parent657d0e99aa8754372786120d6ec00c9d9970e775 (diff)
downloadcpython-f4f530804b9d8f089eba0f157ec2144c03b13651.zip
cpython-f4f530804b9d8f089eba0f157ec2144c03b13651.tar.gz
cpython-f4f530804b9d8f089eba0f157ec2144c03b13651.tar.bz2
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.
Diffstat (limited to 'Include')
-rw-r--r--Include/internal/pycore_object.h44
1 files changed, 44 insertions, 0 deletions
diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h
index ce876b0..6b0b464 100644
--- a/Include/internal/pycore_object.h
+++ b/Include/internal/pycore_object.h
@@ -697,8 +697,52 @@ _PyObject_SetMaybeWeakref(PyObject *op)
}
}
+extern int _PyObject_ResurrectEndSlow(PyObject *op);
#endif
+// Temporarily resurrects an object during deallocation. The refcount is set
+// to one.
+static inline void
+_PyObject_ResurrectStart(PyObject *op)
+{
+ assert(Py_REFCNT(op) == 0);
+#ifdef Py_REF_DEBUG
+ _Py_IncRefTotal(_PyThreadState_GET());
+#endif
+#ifdef Py_GIL_DISABLED
+ _Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_ThreadId());
+ _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 1);
+ _Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, 0);
+#else
+ Py_SET_REFCNT(op, 1);
+#endif
+}
+
+// Undoes an object resurrection by decrementing the refcount without calling
+// _Py_Dealloc(). Returns 0 if the object is dead (the normal case), and
+// deallocation should continue. Returns 1 if the object is still alive.
+static inline int
+_PyObject_ResurrectEnd(PyObject *op)
+{
+#ifdef Py_REF_DEBUG
+ _Py_DecRefTotal(_PyThreadState_GET());
+#endif
+#ifndef Py_GIL_DISABLED
+ Py_SET_REFCNT(op, Py_REFCNT(op) - 1);
+ return Py_REFCNT(op) != 0;
+#else
+ uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local);
+ Py_ssize_t shared = _Py_atomic_load_ssize_acquire(&op->ob_ref_shared);
+ if (_Py_IsOwnedByCurrentThread(op) && local == 1 && shared == 0) {
+ // Fast-path: object has a single refcount and is owned by this thread
+ _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0);
+ return 0;
+ }
+ // Slow-path: object has a shared refcount or is not owned by this thread
+ return _PyObject_ResurrectEndSlow(op);
+#endif
+}
+
/* Tries to incref op and returns 1 if successful or 0 otherwise. */
static inline int
_Py_TryIncref(PyObject *op)