https://github.com/python/cpython/commit/50fcb9193dc199a285fd1f86270e90d8ddbc95bf
commit: 50fcb9193dc199a285fd1f86270e90d8ddbc95bf
branch: main
author: Neil Schemenauer <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-15T10:53:33+05:30
summary:
gh-155400: Fix deadlock with type_lock_prevent_release() (#155401)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-32-15.gh-issue-155400.Kq3Vpt.rst
M Lib/test/test_free_threading/test_type.py
M Objects/typeobject.c
diff --git a/Lib/test/test_free_threading/test_type.py
b/Lib/test/test_free_threading/test_type.py
index f17d12532c63ce..d64a3f38f14830 100644
--- a/Lib/test/test_free_threading/test_type.py
+++ b/Lib/test/test_free_threading/test_type.py
@@ -324,5 +324,63 @@ def wrapper():
for reader in readers:
reader.join()
+ def test_concurrent_setattr_deadlock(self):
+ # gh-155400: two threads assigning to a special method of the same
+ # class could deadlock. One thread held the type lock and waited for
+ # the type dict mutex, which its critical section had released when it
+ # blocked on the stop-the-world mutex, while the other held the type
+ # dict mutex and waited for the type lock.
+ # This is fairly difficult to trigger the race but this N seems to do
+ # it at least sometimes.
+ N = 200
+ done = False
+
+ class Base:
+ pass
+
+ def setter():
+ func = lambda self: "x"
+ barrier.wait()
+ while not done:
+ Base.__repr__ = func
+ try:
+ del Base.__repr__
+ except AttributeError:
+ pass
+
+ def subclasser():
+ barrier.wait()
+ while not done:
+ type('Sub', (Base,), {})()
+
+ def lister():
+ barrier.wait()
+ while not done:
+ Base.__subclasses__()
+
+ def basesetter():
+ nonlocal done
+ barrier.wait()
+ for _ in range(N):
+ class A:
+ pass
+ class C:
+ pass
+ class B(A):
+ pass
+ B.__bases__ = (C,)
+ done = True
+
+ # The setter threads are the ones that deadlock. The others are there
+ # to keep the type lock and the stop-the-world mutex contended, which
+ # is what gets the setters into the window where it happens.
+ targets = (setter, setter, subclasser, subclasser,
+ lister, lister, basesetter)
+ barrier = threading.Barrier(len(targets))
+ threads = [Thread(target=target) for target in targets]
+ with threading_helper.start_threads(threads):
+ pass
+
+
if __name__ == "__main__":
unittest.main()
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-32-15.gh-issue-155400.Kq3Vpt.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-32-15.gh-issue-155400.Kq3Vpt.rst
new file mode 100644
index 00000000000000..813eca65c48e0b
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-32-15.gh-issue-155400.Kq3Vpt.rst
@@ -0,0 +1,5 @@
+Fix a deadlock in the free-threaded build between two threads assigning to a
+special method of the same class. While applying the type slot updates with
+the world stopped, only the type lock was prevented from being released; the
+type dict mutex could still be released and re-acquired, in the wrong order,
+if the thread blocked on the stop-the-world mutex.
diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index 80c84101da967a..420eb855a35d16 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -46,6 +46,13 @@ class object "PyObject *" "&PyBaseObject_Type"
#define NEXT_VERSION_TAG(interp) \
(interp)->types.next_version_tag
+// Storage for the mutexes saved by type_lock_prevent_release(). Defined for
+// both builds so the call sites don't need to be conditionally compiled.
+typedef struct {
+ PyMutex *mutex1;
+ PyMutex *mutex2;
+} pinned_mutexes_t;
+
#ifdef Py_GIL_DISABLED
// There's a global lock for types that ensures that tp_version_tag and
@@ -124,44 +131,54 @@ types_start_world(void)
assert(!types_world_is_stopped());
}
-// This is used to temporarily prevent the TYPE_LOCK from being suspended
-// when held by the topmost critical section.
+// Temporarily prevent the mutexes held by the topmost critical section from
+// being released when the current thread blocks (blocking detaches the thread,
+// which suspends its critical sections and releases the mutexes they hold).
+//
+// All of the mutexes held by the critical section are pinned, not just
+// TYPE_LOCK. If only TYPE_LOCK was pinned then _PyCriticalSection_Resume()
+// would have to re-acquire the other mutex while TYPE_LOCK is held. That
+// deadlocks against a thread that holds that mutex and is waiting for
+// TYPE_LOCK, which is exactly what BEGIN_TYPE_DICT_LOCK() does: the type dict
+// mutex is on the heap and TYPE_LOCK is in _PyRuntime, so the address ordering
+// used by two-mutex critical sections usually acquires the dict mutex first.
+// By pinning both mutexes there is nothing to re-acquire on resume.
+//
+// Holding the mutexes while blocked does not prevent the world from being
+// stopped: a thread waiting on either of them parks with _PY_LOCK_DETACH and
+// so is detached while it waits.
static void
-type_lock_prevent_release(void)
+type_lock_prevent_release(pinned_mutexes_t *pinned)
{
PyThreadState *tstate = _PyThreadState_GET();
- uintptr_t *tagptr = &tstate->critical_section;
- PyCriticalSection *c = (PyCriticalSection *)(*tagptr &
~_Py_CRITICAL_SECTION_MASK);
- if (!(*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
- assert(c->_cs_mutex == TYPE_LOCK);
- c->_cs_mutex = NULL;
- }
- else {
+ uintptr_t tag = tstate->critical_section;
+ PyCriticalSection *c = (PyCriticalSection *)(tag &
~_Py_CRITICAL_SECTION_MASK);
+ pinned->mutex1 = c->_cs_mutex;
+ pinned->mutex2 = NULL;
+ c->_cs_mutex = NULL;
+ if ((tag & _Py_CRITICAL_SECTION_TWO_MUTEXES) != 0) {
PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
- if (c->_cs_mutex == TYPE_LOCK) {
- c->_cs_mutex = c2->_cs_mutex2;
- c2->_cs_mutex2 = NULL;
- } else {
- assert(c2->_cs_mutex2 == TYPE_LOCK);
- c2->_cs_mutex2 = NULL;
- }
+ pinned->mutex2 = c2->_cs_mutex2;
+ c2->_cs_mutex2 = NULL;
}
+ assert(pinned->mutex1 == TYPE_LOCK || pinned->mutex2 == TYPE_LOCK);
}
static void
-type_lock_allow_release(void)
+type_lock_allow_release(pinned_mutexes_t *pinned)
{
PyThreadState *tstate = _PyThreadState_GET();
- uintptr_t *tagptr = &tstate->critical_section;
- PyCriticalSection *c = (PyCriticalSection *)(*tagptr &
~_Py_CRITICAL_SECTION_MASK);
- if (!(*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
- assert(c->_cs_mutex == NULL);
- c->_cs_mutex = TYPE_LOCK;
- }
- else {
+ uintptr_t tag = tstate->critical_section;
+ PyCriticalSection *c = (PyCriticalSection *)(tag &
~_Py_CRITICAL_SECTION_MASK);
+ assert(c->_cs_mutex == NULL);
+ c->_cs_mutex = pinned->mutex1;
+ if ((tag & _Py_CRITICAL_SECTION_TWO_MUTEXES) != 0) {
PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
assert(c2->_cs_mutex2 == NULL);
- c2->_cs_mutex2 = TYPE_LOCK;
+ c2->_cs_mutex2 = pinned->mutex2;
+ }
+ else {
+ assert(pinned->mutex2 == NULL);
}
}
@@ -178,8 +195,8 @@ type_lock_allow_release(void)
#define types_world_is_stopped() 1
#define types_stop_world()
#define types_start_world()
-#define type_lock_prevent_release()
-#define type_lock_allow_release()
+#define type_lock_prevent_release(pinned) ((void)(pinned))
+#define type_lock_allow_release(pinned) ((void)(pinned))
#endif
@@ -650,14 +667,15 @@ set_tp_mro(PyTypeObject *self, PyObject *mro, int initial)
PyUnstable_Object_EnableDeferredRefcount(mro);
}
}
+ pinned_mutexes_t pinned;
if (!initial) {
- type_lock_prevent_release();
+ type_lock_prevent_release(&pinned);
types_stop_world();
}
self->tp_mro = mro;
if (!initial) {
types_start_world();
- type_lock_allow_release();
+ type_lock_allow_release(&pinned);
}
}
@@ -1882,13 +1900,14 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject
*new_bases, PyTypeObject *b
PyObject *old_bases = lookup_tp_bases(type);
assert(old_bases != NULL);
PyTypeObject *old_base = type->tp_base;
+ pinned_mutexes_t pinned;
- type_lock_prevent_release();
+ type_lock_prevent_release(&pinned);
types_stop_world();
set_tp_bases(type, Py_NewRef(new_bases), 0);
type->tp_base = (PyTypeObject *)Py_NewRef(best_base);
types_start_world();
- type_lock_allow_release();
+ type_lock_allow_release(&pinned);
PyObject *temp = PyList_New(0);
if (temp == NULL) {
@@ -1949,12 +1968,12 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject
*new_bases, PyTypeObject *b
if (lookup_tp_bases(type) == new_bases) {
assert(type->tp_base == best_base);
- type_lock_prevent_release();
+ type_lock_prevent_release(&pinned);
types_stop_world();
set_tp_bases(type, old_bases, 0);
type->tp_base = old_base;
types_start_world();
- type_lock_allow_release();
+ type_lock_allow_release(&pinned);
Py_DECREF(new_bases);
Py_DECREF(best_base);
@@ -3862,16 +3881,22 @@ apply_type_slot_updates(slot_update_t *updates)
// to update the dict. That's because TYPE_LOCK was acquired using a
// critical section.
//
- // The type_lock_prevent_release() call prevents the TYPE_LOCK mutex from
- // being released even if we block on the STM mutex. We need to take care
- // that we do not deadlock because of that. It is safe because we always
- // acquire locks in the same order: first the TYPE_LOCK mutex and then the
- // STM mutex.
- type_lock_prevent_release();
+ // The type_lock_prevent_release() call prevents the mutexes held by the
+ // critical section (TYPE_LOCK and the type dict mutex) from being released
+ // even if we block on the STW mutex. We need to take care that we do not
+ // deadlock because of that. It is safe because a thread waiting for
either
+ // of those mutexes detaches while it waits and so does not hold up the
+ // stop-the-world. Pinning both mutexes rather than only TYPE_LOCK is what
+ // makes this safe: otherwise the dict mutex would be released when we
+ // block and _PyCriticalSection_Resume() would have to re-acquire it while
+ // holding TYPE_LOCK, deadlocking with a thread that holds the dict mutex
+ // and is waiting for TYPE_LOCK.
+ pinned_mutexes_t pinned;
+ type_lock_prevent_release(&pinned);
types_stop_world();
apply_slot_updates(updates);
types_start_world();
- type_lock_allow_release();
+ type_lock_allow_release(&pinned);
}
#else
@@ -6364,11 +6389,12 @@ _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned
long mask, unsigned long
}
/* Keep TYPE_LOCK held while waiting for stop-the-world so no thread
can reassign a version tag before the flag update. */
- type_lock_prevent_release();
+ pinned_mutexes_t pinned;
+ type_lock_prevent_release(&pinned);
types_stop_world();
set_flags_recursive(self, mask, flags);
types_start_world();
- type_lock_allow_release();
+ type_lock_allow_release(&pinned);
END_TYPE_LOCK();
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]