Skip to content

Commit 18ff10e

Browse files
committed
gh-155400: Fix deadlock with type_lock_prevent_release()
Two threads assigning to a special method of the same class could deadlock in the free-threaded build. If type_lock_prevent_release() only acts on TYPE_LOCK then we can get lock inversion. This happens when BEGIN_TYPE_DICT_LOCK() is used and the critical section holds both mutexes. We need to prevent release of both mutexes. Assisted-by: Claude
1 parent 549beea commit 18ff10e

3 files changed

Lines changed: 132 additions & 43 deletions

File tree

Lib/test/test_free_threading/test_type.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,5 +324,63 @@ def wrapper():
324324
for reader in readers:
325325
reader.join()
326326

327+
def test_concurrent_setattr_deadlock(self):
328+
# gh-155400: two threads assigning to a special method of the same
329+
# class could deadlock. One thread held the type lock and waited for
330+
# the type dict mutex, which its critical section had released when it
331+
# blocked on the stop-the-world mutex, while the other held the type
332+
# dict mutex and waited for the type lock.
333+
# This is fairly difficult to trigger the race but this N seems to do
334+
# it at least sometimes.
335+
N = 200
336+
done = False
337+
338+
class Base:
339+
pass
340+
341+
def setter():
342+
func = lambda self: "x"
343+
barrier.wait()
344+
while not done:
345+
Base.__repr__ = func
346+
try:
347+
del Base.__repr__
348+
except AttributeError:
349+
pass
350+
351+
def subclasser():
352+
barrier.wait()
353+
while not done:
354+
type('Sub', (Base,), {})()
355+
356+
def lister():
357+
barrier.wait()
358+
while not done:
359+
Base.__subclasses__()
360+
361+
def basesetter():
362+
nonlocal done
363+
barrier.wait()
364+
for _ in range(N):
365+
class A:
366+
pass
367+
class C:
368+
pass
369+
class B(A):
370+
pass
371+
B.__bases__ = (C,)
372+
done = True
373+
374+
# The setter threads are the ones that deadlock. The others are there
375+
# to keep the type lock and the stop-the-world mutex contended, which
376+
# is what gets the setters into the window where it happens.
377+
targets = (setter, setter, subclasser, subclasser,
378+
lister, lister, basesetter)
379+
barrier = threading.Barrier(len(targets))
380+
threads = [Thread(target=target) for target in targets]
381+
with threading_helper.start_threads(threads):
382+
pass
383+
384+
327385
if __name__ == "__main__":
328386
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a deadlock in the free-threaded build between two threads assigning to a
2+
special method of the same class. While applying the type slot updates with
3+
the world stopped, only the type lock was prevented from being released; the
4+
type dict mutex could still be released and re-acquired, in the wrong order,
5+
if the thread blocked on the stop-the-world mutex.

Objects/typeobject.c

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ class object "PyObject *" "&PyBaseObject_Type"
4646
#define NEXT_VERSION_TAG(interp) \
4747
(interp)->types.next_version_tag
4848

49+
// Storage for the mutexes saved by type_lock_prevent_release(). Defined for
50+
// both builds so the call sites don't need to be conditionally compiled.
51+
typedef struct {
52+
PyMutex *mutex1;
53+
PyMutex *mutex2;
54+
} pinned_mutexes_t;
55+
4956
#ifdef Py_GIL_DISABLED
5057

5158
// There's a global lock for types that ensures that tp_version_tag and
@@ -134,44 +141,54 @@ types_start_world(void)
134141
assert(!types_world_is_stopped());
135142
}
136143

137-
// This is used to temporarily prevent the TYPE_LOCK from being suspended
138-
// when held by the topmost critical section.
144+
// Temporarily prevent the mutexes held by the topmost critical section from
145+
// being released when the current thread blocks (blocking detaches the thread,
146+
// which suspends its critical sections and releases the mutexes they hold).
147+
//
148+
// All of the mutexes held by the critical section are pinned, not just
149+
// TYPE_LOCK. If only TYPE_LOCK was pinned then _PyCriticalSection_Resume()
150+
// would have to re-acquire the other mutex while TYPE_LOCK is held. That
151+
// deadlocks against a thread that holds that mutex and is waiting for
152+
// TYPE_LOCK, which is exactly what BEGIN_TYPE_DICT_LOCK() does: the type dict
153+
// mutex is on the heap and TYPE_LOCK is in _PyRuntime, so the address ordering
154+
// used by two-mutex critical sections usually acquires the dict mutex first.
155+
// By pinning both mutexes there is nothing to re-acquire on resume.
156+
//
157+
// Holding the mutexes while blocked does not prevent the world from being
158+
// stopped: a thread waiting on either of them parks with _PY_LOCK_DETACH and
159+
// so is detached while it waits.
139160
static void
140-
type_lock_prevent_release(void)
161+
type_lock_prevent_release(pinned_mutexes_t *pinned)
141162
{
142163
PyThreadState *tstate = _PyThreadState_GET();
143-
uintptr_t *tagptr = &tstate->critical_section;
144-
PyCriticalSection *c = (PyCriticalSection *)(*tagptr & ~_Py_CRITICAL_SECTION_MASK);
145-
if (!(*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
146-
assert(c->_cs_mutex == TYPE_LOCK);
147-
c->_cs_mutex = NULL;
148-
}
149-
else {
164+
uintptr_t tag = tstate->critical_section;
165+
PyCriticalSection *c = (PyCriticalSection *)(tag & ~_Py_CRITICAL_SECTION_MASK);
166+
pinned->mutex1 = c->_cs_mutex;
167+
pinned->mutex2 = NULL;
168+
c->_cs_mutex = NULL;
169+
if ((tag & _Py_CRITICAL_SECTION_TWO_MUTEXES) != 0) {
150170
PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
151-
if (c->_cs_mutex == TYPE_LOCK) {
152-
c->_cs_mutex = c2->_cs_mutex2;
153-
c2->_cs_mutex2 = NULL;
154-
} else {
155-
assert(c2->_cs_mutex2 == TYPE_LOCK);
156-
c2->_cs_mutex2 = NULL;
157-
}
171+
pinned->mutex2 = c2->_cs_mutex2;
172+
c2->_cs_mutex2 = NULL;
158173
}
174+
assert(pinned->mutex1 == TYPE_LOCK || pinned->mutex2 == TYPE_LOCK);
159175
}
160176

161177
static void
162-
type_lock_allow_release(void)
178+
type_lock_allow_release(pinned_mutexes_t *pinned)
163179
{
164180
PyThreadState *tstate = _PyThreadState_GET();
165-
uintptr_t *tagptr = &tstate->critical_section;
166-
PyCriticalSection *c = (PyCriticalSection *)(*tagptr & ~_Py_CRITICAL_SECTION_MASK);
167-
if (!(*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
168-
assert(c->_cs_mutex == NULL);
169-
c->_cs_mutex = TYPE_LOCK;
170-
}
171-
else {
181+
uintptr_t tag = tstate->critical_section;
182+
PyCriticalSection *c = (PyCriticalSection *)(tag & ~_Py_CRITICAL_SECTION_MASK);
183+
assert(c->_cs_mutex == NULL);
184+
c->_cs_mutex = pinned->mutex1;
185+
if ((tag & _Py_CRITICAL_SECTION_TWO_MUTEXES) != 0) {
172186
PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
173187
assert(c2->_cs_mutex2 == NULL);
174-
c2->_cs_mutex2 = TYPE_LOCK;
188+
c2->_cs_mutex2 = pinned->mutex2;
189+
}
190+
else {
191+
assert(pinned->mutex2 == NULL);
175192
}
176193
}
177194

@@ -189,8 +206,8 @@ type_lock_allow_release(void)
189206
#define types_world_is_stopped() 1
190207
#define types_stop_world()
191208
#define types_start_world()
192-
#define type_lock_prevent_release()
193-
#define type_lock_allow_release()
209+
#define type_lock_prevent_release(pinned) ((void)(pinned))
210+
#define type_lock_allow_release(pinned) ((void)(pinned))
194211

195212
#endif
196213

@@ -661,14 +678,15 @@ set_tp_mro(PyTypeObject *self, PyObject *mro, int initial)
661678
PyUnstable_Object_EnableDeferredRefcount(mro);
662679
}
663680
}
681+
pinned_mutexes_t pinned;
664682
if (!initial) {
665-
type_lock_prevent_release();
683+
type_lock_prevent_release(&pinned);
666684
types_stop_world();
667685
}
668686
self->tp_mro = mro;
669687
if (!initial) {
670688
types_start_world();
671-
type_lock_allow_release();
689+
type_lock_allow_release(&pinned);
672690
}
673691
}
674692

@@ -1894,13 +1912,14 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject *b
18941912
PyObject *old_bases = lookup_tp_bases(type);
18951913
assert(old_bases != NULL);
18961914
PyTypeObject *old_base = type->tp_base;
1915+
pinned_mutexes_t pinned;
18971916

1898-
type_lock_prevent_release();
1917+
type_lock_prevent_release(&pinned);
18991918
types_stop_world();
19001919
set_tp_bases(type, Py_NewRef(new_bases), 0);
19011920
type->tp_base = (PyTypeObject *)Py_NewRef(best_base);
19021921
types_start_world();
1903-
type_lock_allow_release();
1922+
type_lock_allow_release(&pinned);
19041923

19051924
PyObject *temp = PyList_New(0);
19061925
if (temp == NULL) {
@@ -1961,12 +1980,12 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject *b
19611980
if (lookup_tp_bases(type) == new_bases) {
19621981
assert(type->tp_base == best_base);
19631982

1964-
type_lock_prevent_release();
1983+
type_lock_prevent_release(&pinned);
19651984
types_stop_world();
19661985
set_tp_bases(type, old_bases, 0);
19671986
type->tp_base = old_base;
19681987
types_start_world();
1969-
type_lock_allow_release();
1988+
type_lock_allow_release(&pinned);
19701989

19711990
Py_DECREF(new_bases);
19721991
Py_DECREF(best_base);
@@ -3874,16 +3893,22 @@ apply_type_slot_updates(slot_update_t *updates)
38743893
// to update the dict. That's because TYPE_LOCK was acquired using a
38753894
// critical section.
38763895
//
3877-
// The type_lock_prevent_release() call prevents the TYPE_LOCK mutex from
3878-
// being released even if we block on the STM mutex. We need to take care
3879-
// that we do not deadlock because of that. It is safe because we always
3880-
// acquire locks in the same order: first the TYPE_LOCK mutex and then the
3881-
// STM mutex.
3882-
type_lock_prevent_release();
3896+
// The type_lock_prevent_release() call prevents the mutexes held by the
3897+
// critical section (TYPE_LOCK and the type dict mutex) from being released
3898+
// even if we block on the STW mutex. We need to take care that we do not
3899+
// deadlock because of that. It is safe because a thread waiting for either
3900+
// of those mutexes detaches while it waits and so does not hold up the
3901+
// stop-the-world. Pinning both mutexes rather than only TYPE_LOCK is what
3902+
// makes this safe: otherwise the dict mutex would be released when we
3903+
// block and _PyCriticalSection_Resume() would have to re-acquire it while
3904+
// holding TYPE_LOCK, deadlocking with a thread that holds the dict mutex
3905+
// and is waiting for TYPE_LOCK.
3906+
pinned_mutexes_t pinned;
3907+
type_lock_prevent_release(&pinned);
38833908
types_stop_world();
38843909
apply_slot_updates(updates);
38853910
types_start_world();
3886-
type_lock_allow_release();
3911+
type_lock_allow_release(&pinned);
38873912
}
38883913

38893914
#else
@@ -6404,11 +6429,12 @@ _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned long
64046429
}
64056430
/* Keep TYPE_LOCK held while waiting for stop-the-world so no thread
64066431
can reassign a version tag before the flag update. */
6407-
type_lock_prevent_release();
6432+
pinned_mutexes_t pinned;
6433+
type_lock_prevent_release(&pinned);
64086434
types_stop_world();
64096435
set_flags_recursive(self, mask, flags);
64106436
types_start_world();
6411-
type_lock_allow_release();
6437+
type_lock_allow_release(&pinned);
64126438
END_TYPE_LOCK();
64136439
}
64146440

0 commit comments

Comments
 (0)