Skip to content

Commit da24ef3

Browse files
naschememiss-islington
authored andcommitted
gh-155400: Fix deadlock with type_lock_prevent_release() (GH-155401)
(cherry picked from commit 50fcb91) Co-authored-by: Neil Schemenauer <nas-github@arctrix.com>
1 parent 042df0a commit da24ef3

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
@@ -181,5 +181,63 @@ def wrapper():
181181
for reader in readers:
182182
reader.join()
183183

184+
def test_concurrent_setattr_deadlock(self):
185+
# gh-155400: two threads assigning to a special method of the same
186+
# class could deadlock. One thread held the type lock and waited for
187+
# the type dict mutex, which its critical section had released when it
188+
# blocked on the stop-the-world mutex, while the other held the type
189+
# dict mutex and waited for the type lock.
190+
# This is fairly difficult to trigger the race but this N seems to do
191+
# it at least sometimes.
192+
N = 200
193+
done = False
194+
195+
class Base:
196+
pass
197+
198+
def setter():
199+
func = lambda self: "x"
200+
barrier.wait()
201+
while not done:
202+
Base.__repr__ = func
203+
try:
204+
del Base.__repr__
205+
except AttributeError:
206+
pass
207+
208+
def subclasser():
209+
barrier.wait()
210+
while not done:
211+
type('Sub', (Base,), {})()
212+
213+
def lister():
214+
barrier.wait()
215+
while not done:
216+
Base.__subclasses__()
217+
218+
def basesetter():
219+
nonlocal done
220+
barrier.wait()
221+
for _ in range(N):
222+
class A:
223+
pass
224+
class C:
225+
pass
226+
class B(A):
227+
pass
228+
B.__bases__ = (C,)
229+
done = True
230+
231+
# The setter threads are the ones that deadlock. The others are there
232+
# to keep the type lock and the stop-the-world mutex contended, which
233+
# is what gets the setters into the window where it happens.
234+
targets = (setter, setter, subclasser, subclasser,
235+
lister, lister, basesetter)
236+
barrier = threading.Barrier(len(targets))
237+
threads = [Thread(target=target) for target in targets]
238+
with threading_helper.start_threads(threads):
239+
pass
240+
241+
184242
if __name__ == "__main__":
185243
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
@@ -60,6 +60,13 @@ class object "PyObject *" "&PyBaseObject_Type"
6060
#define NEXT_VERSION_TAG(interp) \
6161
(interp)->types.next_version_tag
6262

63+
// Storage for the mutexes saved by type_lock_prevent_release(). Defined for
64+
// both builds so the call sites don't need to be conditionally compiled.
65+
typedef struct {
66+
PyMutex *mutex1;
67+
PyMutex *mutex2;
68+
} pinned_mutexes_t;
69+
6370
#ifdef Py_GIL_DISABLED
6471

6572
// There's a global lock for types that ensures that tp_version_tag and
@@ -138,44 +145,54 @@ types_start_world(void)
138145
assert(!types_world_is_stopped());
139146
}
140147

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

165181
static void
166-
type_lock_allow_release(void)
182+
type_lock_allow_release(pinned_mutexes_t *pinned)
167183
{
168184
PyThreadState *tstate = _PyThreadState_GET();
169-
uintptr_t *tagptr = &tstate->critical_section;
170-
PyCriticalSection *c = (PyCriticalSection *)(*tagptr & ~_Py_CRITICAL_SECTION_MASK);
171-
if (!(*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
172-
assert(c->_cs_mutex == NULL);
173-
c->_cs_mutex = TYPE_LOCK;
174-
}
175-
else {
185+
uintptr_t tag = tstate->critical_section;
186+
PyCriticalSection *c = (PyCriticalSection *)(tag & ~_Py_CRITICAL_SECTION_MASK);
187+
assert(c->_cs_mutex == NULL);
188+
c->_cs_mutex = pinned->mutex1;
189+
if ((tag & _Py_CRITICAL_SECTION_TWO_MUTEXES) != 0) {
176190
PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
177191
assert(c2->_cs_mutex2 == NULL);
178-
c2->_cs_mutex2 = TYPE_LOCK;
192+
c2->_cs_mutex2 = pinned->mutex2;
193+
}
194+
else {
195+
assert(pinned->mutex2 == NULL);
179196
}
180197
}
181198

@@ -192,8 +209,8 @@ type_lock_allow_release(void)
192209
#define types_world_is_stopped() 1
193210
#define types_stop_world()
194211
#define types_start_world()
195-
#define type_lock_prevent_release()
196-
#define type_lock_allow_release()
212+
#define type_lock_prevent_release(pinned) ((void)(pinned))
213+
#define type_lock_allow_release(pinned) ((void)(pinned))
197214

198215
#endif
199216

@@ -664,14 +681,15 @@ set_tp_mro(PyTypeObject *self, PyObject *mro, int initial)
664681
PyUnstable_Object_EnableDeferredRefcount(mro);
665682
}
666683
}
684+
pinned_mutexes_t pinned;
667685
if (!initial) {
668-
type_lock_prevent_release();
686+
type_lock_prevent_release(&pinned);
669687
types_stop_world();
670688
}
671689
self->tp_mro = mro;
672690
if (!initial) {
673691
types_start_world();
674-
type_lock_allow_release();
692+
type_lock_allow_release(&pinned);
675693
}
676694
}
677695

@@ -1934,13 +1952,14 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject *b
19341952
PyObject *old_bases = lookup_tp_bases(type);
19351953
assert(old_bases != NULL);
19361954
PyTypeObject *old_base = type->tp_base;
1955+
pinned_mutexes_t pinned;
19371956

1938-
type_lock_prevent_release();
1957+
type_lock_prevent_release(&pinned);
19391958
types_stop_world();
19401959
set_tp_bases(type, Py_NewRef(new_bases), 0);
19411960
type->tp_base = (PyTypeObject *)Py_NewRef(best_base);
19421961
types_start_world();
1943-
type_lock_allow_release();
1962+
type_lock_allow_release(&pinned);
19441963

19451964
PyObject *temp = PyList_New(0);
19461965
if (temp == NULL) {
@@ -2001,12 +2020,12 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject *b
20012020
if (lookup_tp_bases(type) == new_bases) {
20022021
assert(type->tp_base == best_base);
20032022

2004-
type_lock_prevent_release();
2023+
type_lock_prevent_release(&pinned);
20052024
types_stop_world();
20062025
set_tp_bases(type, old_bases, 0);
20072026
type->tp_base = old_base;
20082027
types_start_world();
2009-
type_lock_allow_release();
2028+
type_lock_allow_release(&pinned);
20102029

20112030
Py_DECREF(new_bases);
20122031
Py_DECREF(best_base);
@@ -3914,16 +3933,22 @@ apply_type_slot_updates(slot_update_t *updates)
39143933
// to update the dict. That's because TYPE_LOCK was acquired using a
39153934
// critical section.
39163935
//
3917-
// The type_lock_prevent_release() call prevents the TYPE_LOCK mutex from
3918-
// being released even if we block on the STM mutex. We need to take care
3919-
// that we do not deadlock because of that. It is safe because we always
3920-
// acquire locks in the same order: first the TYPE_LOCK mutex and then the
3921-
// STM mutex.
3922-
type_lock_prevent_release();
3936+
// The type_lock_prevent_release() call prevents the mutexes held by the
3937+
// critical section (TYPE_LOCK and the type dict mutex) from being released
3938+
// even if we block on the STW mutex. We need to take care that we do not
3939+
// deadlock because of that. It is safe because a thread waiting for either
3940+
// of those mutexes detaches while it waits and so does not hold up the
3941+
// stop-the-world. Pinning both mutexes rather than only TYPE_LOCK is what
3942+
// makes this safe: otherwise the dict mutex would be released when we
3943+
// block and _PyCriticalSection_Resume() would have to re-acquire it while
3944+
// holding TYPE_LOCK, deadlocking with a thread that holds the dict mutex
3945+
// and is waiting for TYPE_LOCK.
3946+
pinned_mutexes_t pinned;
3947+
type_lock_prevent_release(&pinned);
39233948
types_stop_world();
39243949
apply_slot_updates(updates);
39253950
types_start_world();
3926-
type_lock_allow_release();
3951+
type_lock_allow_release(&pinned);
39273952
}
39283953

39293954
#else
@@ -6518,11 +6543,12 @@ _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned long
65186543
}
65196544
/* Keep TYPE_LOCK held while waiting for stop-the-world so no thread
65206545
can reassign a version tag before the flag update. */
6521-
type_lock_prevent_release();
6546+
pinned_mutexes_t pinned;
6547+
type_lock_prevent_release(&pinned);
65226548
types_stop_world();
65236549
set_flags_recursive(self, mask, flags);
65246550
types_start_world();
6525-
type_lock_allow_release();
6551+
type_lock_allow_release(&pinned);
65266552
END_TYPE_LOCK();
65276553
}
65286554

0 commit comments

Comments
 (0)