https://github.com/python/cpython/commit/b89f1c95da70a14dc07485a49e93280df6c6ffbd
commit: b89f1c95da70a14dc07485a49e93280df6c6ffbd
branch: main
author: Neil Schemenauer <[email protected]>
committer: nascheme <[email protected]>
date: 2026-11-02T09:20:00-07:00
summary:
gh-158195: Do not hold the type lock during stop-the-world (gh-159972)
Fix a free-threaded deadlock caused by holding TYPE_LOCK while waiting for
the stop-the-world mutex: its owner may need TYPE_LOCK to reattach after
running Python code during a type update. Allow TYPE_LOCK to be released
while waiting, and keep slot updates and bases validation and publication
within a single stop-the-world pause so they cannot become stale. Ensure this
work cannot invoke user code or deallocate objects, avoiding re-entrancy while
the world is stopped.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-10-00-00.gh-issue-158195.UnpinTL.rst
M Include/internal/pycore_dict.h
M Lib/test/test_descr.py
M Lib/test/test_free_threading/test_threading.py
M Lib/test/test_free_threading/test_type.py
M Modules/_testinternalcapi.c
M Objects/dictobject.c
M Objects/typeobject.c
diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h
index 0257aaaf4534d5e..a30a79f31e6d968 100644
--- a/Include/internal/pycore_dict.h
+++ b/Include/internal/pycore_dict.h
@@ -170,6 +170,10 @@ PyAPI_FUNC(int)
_PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *k
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash_LockHeld(PyDictObject *op,
PyObject *key, Py_hash_t hash, PyObject **result);
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key,
Py_hash_t hash, PyObject **result);
extern int _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject
*key, PyObject **result);
+extern PyObject *_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key);
+extern int _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp,
+ PyObject *key,
+ _PyStackRef *value_addr);
PyAPI_FUNC(int) _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj,
PyObject **dictptr, PyObject *name, PyObject *value);
extern int _PyDict_Pop_KnownHash(
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 93b902a632de354..c4c4d0858ec00ba 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -5098,6 +5098,31 @@ def __new__(mcls, name, bases, ns):
with self.assertWarnsRegex(RuntimeWarning, 'MyClass'):
MyClass = meta('MyClass', (), {})
+ def test_str_subclass_namespace_keys(self):
+ # Keys of the class namespace that are str subclasses are copied
+ # to exact str in the type's __dict__, like type.__setattr__ does.
+ class Key(str):
+ def __hash__(self):
+ # Collide with the slot name assigned below.
+ return hash('__hash__')
+ def __eq__(self, other):
+ # Must not be called by type dict lookups.
+ raise AssertionError('Key.__eq__ called')
+
+ ns = {Key('other'): 1, '__repr__': lambda self: 'r', 'c': 2}
+ C = type('C', (), ns)
+ for key in C.__dict__:
+ self.assertIs(type(key), str)
+ self.assertEqual([str(k) for k in ns], list(C.__dict__)[:3])
+ self.assertEqual(repr(C()), 'r')
+ self.assertEqual(C.other, 1)
+ # Lookups and slot updates that hash-collide with 'other' do not
+ # call Key.__eq__.
+ C.__hash__ = None
+ with self.assertRaises(TypeError):
+ hash(C())
+ del C.__hash__
+
def test_proxy_call(self):
class FakeStr:
__class__ = str
diff --git a/Lib/test/test_free_threading/test_threading.py
b/Lib/test/test_free_threading/test_threading.py
index ecc42db7b44f3af..670d66e1d0ebb23 100644
--- a/Lib/test/test_free_threading/test_threading.py
+++ b/Lib/test/test_free_threading/test_threading.py
@@ -26,6 +26,30 @@ def mutate_thread():
class TestThreadState(unittest.TestCase):
+ def test_stop_the_world(self):
+ import _testinternalcapi
+
+ _testinternalcapi.stop_the_world()
+ _testinternalcapi.stop_the_world(0)
+
+ def pause():
+ for _ in range(10):
+ _testinternalcapi.stop_the_world(0.0001)
+
+ threading_helper.run_concurrently(pause, nthreads=2)
+ for duration in (-1, -0.1, float("nan")):
+ with self.subTest(duration=duration):
+ with self.assertRaises(ValueError):
+ _testinternalcapi.stop_the_world(duration)
+ for duration in (float("inf"), 10**100):
+ with self.subTest(duration=duration):
+ with self.assertRaises(OverflowError):
+ _testinternalcapi.stop_the_world(duration)
+ for duration in (None, "0"):
+ with self.subTest(duration=duration):
+ with self.assertRaises(TypeError):
+ _testinternalcapi.stop_the_world(duration)
+
@support.requires_subprocess()
def test_tight_stw_loop_does_not_starve_attach(self):
script = textwrap.dedent(f"""
diff --git a/Lib/test/test_free_threading/test_type.py
b/Lib/test/test_free_threading/test_type.py
index 77af1c1e9d387f7..6e750e0bc6084e5 100644
--- a/Lib/test/test_free_threading/test_type.py
+++ b/Lib/test/test_free_threading/test_type.py
@@ -1,3 +1,4 @@
+import gc
import threading
import unittest
@@ -19,6 +20,24 @@
class A:
attr = 1
+def world_stoppers():
+ """Finite workloads that contend for the stop-the-world mutex.
+
+ Waiting for the mutex releases the type lock, so that other threads
+ can change types in the meantime. Use a short pause rather than full
collections
+ to prolong the contention without depending on the size of the heap.
+ """
+ def stopper():
+ for _ in range(100):
+ _testinternalcapi.stop_the_world()
+
+ def pauser():
+ for _ in range(100):
+ _testinternalcapi.stop_the_world(0.0001)
+
+ return [stopper, pauser]
+
+
@threading_helper.requires_working_threading()
class TestType(TestCase):
def test_attr_cache(self):
@@ -325,9 +344,8 @@ def wrapper():
reader.join()
def test_setattr_many_subclasses(self):
- # gh-155978: Updating a special method queues a slot update for every
- # affected subclass. Keep enough subclasses alive to require
- # heap-allocated queue chunks in addition to the stack chunk.
+ # gh-155978: Updating a special method updates the slot of every
+ # affected subclass.
class Base:
pass
@@ -344,6 +362,11 @@ def custom_repr(self):
self.assertTrue(all(repr(cls()) != "custom repr"
for cls in subclasses))
+ # The slot update must not keep references to the types.
+ del subclasses
+ gc.collect()
+ self.assertEqual(Base.__subclasses__(), [])
+
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
@@ -401,6 +424,151 @@ class B(A):
with threading_helper.start_threads(threads):
pass
+ def check_stop_the_world_under_type_lock(self, setter):
+ # gh-158195: Python code that runs with the type lock held and stops
+ # the world could deadlock with a thread that held the type lock
+ # pinned while it waited for the stop-the-world mutex (__bases__
+ # assignment). The first thread released the type lock while it was
+ # blocked on the stop-the-world mutex, was handed the mutex, and then
+ # waited for the type lock while owning the mutex.
+ def basesetter():
+ class A:
+ pass
+ class B:
+ pass
+ class C(A):
+ pass
+ for _ in range(100):
+ C.__bases__ = (B,)
+ C.__bases__ = (A,)
+
+ def stopper():
+ for _ in range(100):
+ _testinternalcapi.stop_the_world()
+
+ # The stopper keeps the stop-the-world mutex contended, which makes
+ # the setters block on it. All workers have finite workloads so
+ # that repeated invalidation cannot indefinitely delay a setter.
+ threading_helper.run_concurrently(
+ [setter, setter, basesetter, stopper])
+
+ def test_stop_the_world_in_mro_deadlock(self):
+ # type_ready() calls a metaclass mro() with the type lock held.
+ N = 50
+
+ class Meta(type):
+ def mro(cls):
+ _testinternalcapi.stop_the_world()
+ return type.mro(cls)
+
+ def setter():
+ for _ in range(N):
+ Meta("K", (), {})
+
+ self.check_stop_the_world_under_type_lock(setter)
+
+ def test_reentrant_mro_always_publishes(self):
+ # A custom mro() that updates a slot on every call must not make
+ # the __bases__ assignment retry forever.
+ class Other:
+ pass
+
+ class Meta(type):
+ def mro(cls):
+ Other.__repr__ = lambda self: "x"
+ return type.mro(cls)
+
+ class A:
+ pass
+
+ class B:
+ pass
+
+ class C(A, metaclass=Meta):
+ pass
+
+ C.__bases__ = (B,)
+ self.assertEqual(C.__mro__, (C, B, object))
+
+ def test_slot_update_skips_non_str_keys(self):
+ # Slots are computed (with the world stopped, for an update) without
+ # comparing the slot name with non-str keys of the type dicts, which
+ # could run user code. Here, that code would also update a slot
+ # every time.
+ calls = 0
+
+ class Other:
+ pass
+
+ class Key:
+ def __hash__(self):
+ return hash("__hash__")
+
+ def __eq__(self, other):
+ nonlocal calls
+ calls += 1
+ Other.__repr__ = lambda self: "x"
+ return False
+
+ class Base:
+ pass
+
+ with self.assertWarns(RuntimeWarning):
+ Sub = type("Sub", (Base,), {Key(): None})
+ self.assertEqual(calls, 0)
+
+ Base.__hash__ = None
+ self.assertEqual(calls, 0)
+ with self.assertRaises(TypeError):
+ hash(Sub())
+ del Base.__hash__
+ hash(Sub())
+
+ def test_concurrent_bases_assignment(self):
+ # Several threads reassign __bases__ of the same class, and of its
+ # base, while others stop the world. The type lock is released while
+ # waiting for the stop-the-world. At the end, the MRO must agree
+ # with __bases__.
+ N = 50
+
+ class A:
+ def meth(self):
+ return "A"
+ def __repr__(self):
+ return "A"
+ class B:
+ def meth(self):
+ return "B"
+ def __repr__(self):
+ return "B"
+ class C(A):
+ pass
+ class D(C):
+ pass
+
+ def setter():
+ for _ in range(N):
+ C.__bases__ = (B,)
+ C.__bases__ = (A,)
+
+ def subsetter():
+ for _ in range(N):
+ D.__bases__ = (C, B)
+ D.__bases__ = (C,)
+
+ threading_helper.run_concurrently(
+ [setter, setter, subsetter] + world_stoppers())
+
+ self.assertEqual(C.__bases__, (A,))
+ self.assertEqual(C.__mro__, (C, A, object))
+ self.assertEqual(D.__bases__, (C,))
+ self.assertEqual(D.__mro__, (D, C, A, object))
+ self.assertEqual(D().meth(), "A")
+ self.assertEqual(repr(C()), "A")
+ self.assertEqual(repr(D()), "A")
+ self.assertIn(C, A.__subclasses__())
+ self.assertNotIn(C, B.__subclasses__())
+
def test_class_creation_with_concurrent_setattr(self):
# Creating a class resolves its slots from the dicts along the MRO
# while holding the type lock, before the class is reachable from
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-10-00-00.gh-issue-158195.UnpinTL.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-10-00-00.gh-issue-158195.UnpinTL.rst
new file mode 100644
index 000000000000000..f189c7c563ca58c
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-10-00-00.gh-issue-158195.UnpinTL.rst
@@ -0,0 +1,7 @@
+Fix a deadlock in the free-threaded build between a thread that stops the
+world while running Python code under the type lock (for example a custom
+metaclass ``mro()`` or the ``__eq__`` of a :class:`str` subclass used as a
+class ``__dict__`` key) and a thread that assigns to a special method or to
+``__bases__`` of a class. The type lock is no longer held while waiting
+for a stop-the-world; the prepared slot, MRO and bases updates are
+validated with the world stopped and recomputed if they went stale.
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index b3945b1a7634dc8..3d0058fc11c452f 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -38,6 +38,7 @@
#include "pycore_pylifecycle.h" // _PyInterpreterConfig_InitFromDict()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS
+#include "pycore_time.h" // _PyTime_FromSecondsObject()
#include "pycore_unicodeobject.h" //
_PyUnicode_TransformDecimalAndSpaceToASCII()
#include "clinic/_testinternalcapi.c.h"
@@ -209,6 +210,42 @@ get_stack_margin(PyObject *self, PyObject *Py_UNUSED(args))
return PyLong_FromSize_t(_PyOS_STACK_MARGIN_BYTES);
}
+static PyObject *
+stop_the_world(PyObject *self, PyObject *args)
+{
+ PyObject *duration_obj = NULL;
+ PyTime_t duration = 0;
+ if (!PyArg_ParseTuple(args, "|O:stop_the_world", &duration_obj)) {
+ return NULL;
+ }
+ if (duration_obj != NULL &&
+ _PyTime_FromSecondsObject(&duration, duration_obj,
+ _PyTime_ROUND_TIMEOUT) < 0) {
+ return NULL;
+ }
+ if (duration < 0) {
+ PyErr_SetString(PyExc_ValueError, "duration must be non-negative");
+ return NULL;
+ }
+#ifdef Py_GIL_DISABLED
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ _PyEval_StopTheWorld(interp);
+ if (duration != 0) {
+ PyEvent event = {0};
+ PyEvent_WaitTimed(&event, duration, /*detach=*/0);
+ }
+ _PyEval_StartTheWorld(interp);
+#endif
+ Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(stop_the_world_doc,
+"stop_the_world($module, duration=0, /)\n"
+"--\n"
+"\n"
+"Stop the world, wait for duration seconds, then restart it.\n"
+"Does nothing in GIL-enabled builds.");
+
static PyObject *
test_stop_the_world(PyObject *self, PyObject *Py_UNUSED(args))
{
@@ -3363,6 +3400,7 @@ static PyMethodDef module_functions[] = {
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},
{"get_stack_margin", get_stack_margin, METH_NOARGS},
+ {"stop_the_world", stop_the_world, METH_VARARGS, stop_the_world_doc},
{"test_stop_the_world", test_stop_the_world, METH_NOARGS},
{"classify_stack_addresses", classify_stack_addresses, METH_VARARGS},
{"get_jit_code_ranges", get_jit_code_ranges, METH_NOARGS},
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index fa6fd6b27ef8794..792602184b44f81 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -2631,6 +2631,81 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject
**result)
return _PyDict_GetItemRef_KnownHash((PyDictObject *)op, key, hash, result);
}
+static inline int
+compare_generic_exact_unicode(PyDictObject *mp, PyDictKeysObject *dk,
+ void *ep0, Py_ssize_t ix, PyObject *key,
+ Py_hash_t hash)
+{
+ PyDictKeyEntry *ep = &((PyDictKeyEntry *)ep0)[ix];
+ assert(ep->me_key != NULL);
+ if (ep->me_key == key) {
+ return 1;
+ }
+ return (ep->me_hash == hash && PyUnicode_CheckExact(ep->me_key) &&
+ unicode_eq(ep->me_key, key));
+}
+
+// Look up an exact str key, without comparing it to keys that are not exact
+// str: those are skipped, so no user code can run. Returns a borrowed
+// reference, or NULL (without an exception) if the key is not found. The
+// caller must hold the dict lock or have the world stopped.
+PyObject *
+_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key)
+{
+ assert(PyUnicode_CheckExact(key));
+ Py_hash_t hash = hash_unicode_key(key);
+ PyDictKeysObject *dk = mp->ma_keys;
+ Py_ssize_t ix;
+ if (dk->dk_kind == DICT_KEYS_GENERAL) {
+ ix = do_lookup(mp, dk, key, hash, compare_generic_exact_unicode);
+ }
+ else {
+ ix = unicodekeys_lookup_unicode(dk, key, hash);
+ }
+ if (ix < 0) {
+ return NULL;
+ }
+ if (dk->dk_kind == DICT_KEYS_SPLIT) {
+ return mp->ma_values->values[ix];
+ }
+ if (dk->dk_kind == DICT_KEYS_GENERAL) {
+ return DK_ENTRIES(dk)[ix].me_value;
+ }
+ return DK_UNICODE_ENTRIES(dk)[ix].me_value;
+}
+
+// Like _PyDict_GetItem_ExactUnicode() but safe to call without holding the
+// dict lock. Stores a new reference (or NULL) in *value_addr. Returns 1 if
+// the key was found and 0 if not.
+int
+_PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp, PyObject *key,
+ _PyStackRef *value_addr)
+{
+ assert(PyUnicode_CheckExact(key));
+ PyObject *value;
+#ifdef Py_GIL_DISABLED
+ ensure_shared_on_read(mp);
+ PyDictKeysObject *dk = _Py_atomic_load_ptr_acquire(&mp->ma_keys);
+ if (dk->dk_kind == DICT_KEYS_UNICODE) {
+ // All keys are exact str, so the lock-free lookup cannot run user
+ // code.
+ Py_hash_t hash = hash_unicode_key(key);
+ Py_ssize_t ix = lookup_threadsafe_unicode(dk, key, hash, value_addr);
+ if (ix != DKIX_KEY_CHANGED) {
+ return !PyStackRef_IsNull(*value_addr);
+ }
+ }
+ Py_BEGIN_CRITICAL_SECTION(mp);
+ value = _PyDict_GetItem_ExactUnicode(mp, key);
+ *value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
+ Py_END_CRITICAL_SECTION();
+#else
+ value = _PyDict_GetItem_ExactUnicode(mp, key);
+ *value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
+#endif
+ return value != NULL;
+}
+
int
_PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject
**result)
{
diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index 22be94bacbc0a36..66c7bc7f8902fc3 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -46,13 +46,6 @@ 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
@@ -77,7 +70,6 @@ typedef struct {
#define END_TYPE_DICT_LOCK() Py_END_CRITICAL_SECTION2()
-#if !defined(NDEBUG) || defined(Py_DEBUG)
// Return true if the world is currently stopped.
static bool
types_world_is_stopped(void)
@@ -85,7 +77,6 @@ types_world_is_stopped(void)
PyInterpreterState *interp = _PyInterpreterState_GET();
return interp->stoptheworld.world_stopped;
}
-#endif
// Checks that the type has not yet been revealed (exposed) to other
// threads. The _Py_TYPE_REVEALED_FLAG flag is set by type_new() and
@@ -131,57 +122,6 @@ types_start_world(void)
assert(!types_world_is_stopped());
}
-// 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(pinned_mutexes_t *pinned)
-{
- PyThreadState *tstate = _PyThreadState_GET();
- 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;
- pinned->mutex2 = c2->_cs_mutex2;
- c2->_cs_mutex2 = NULL;
- }
- assert(pinned->mutex1 == TYPE_LOCK || pinned->mutex2 == TYPE_LOCK);
-}
-
-static void
-type_lock_allow_release(pinned_mutexes_t *pinned)
-{
- PyThreadState *tstate = _PyThreadState_GET();
- 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 = pinned->mutex2;
- }
- else {
- assert(pinned->mutex2 == NULL);
- }
-}
-
#else
#define BEGIN_TYPE_LOCK()
@@ -195,8 +135,6 @@ type_lock_allow_release(pinned_mutexes_t *pinned)
#define types_world_is_stopped() 1
#define types_stop_world()
#define types_start_world()
-#define type_lock_prevent_release(pinned) ((void)(pinned))
-#define type_lock_allow_release(pinned) ((void)(pinned))
#endif
@@ -599,7 +537,7 @@ _PyType_GetBases(PyTypeObject *self)
}
static inline void
-set_tp_bases(PyTypeObject *self, PyObject *bases, int initial)
+set_tp_bases(PyTypeObject *self, PyObject *bases)
{
assert(PyTuple_Check(bases));
ASSERT_NEW_TYPE_OR_LOCKED(self);
@@ -607,7 +545,6 @@ set_tp_bases(PyTypeObject *self, PyObject *bases, int
initial)
// XXX tp_bases can probably be statically allocated for each
// static builtin type.
assert(PyTuple_CheckExact(bases));
- assert(initial);
assert(self->tp_bases == NULL);
if (PyTuple_GET_SIZE(bases) == 0) {
assert(self->tp_base == NULL);
@@ -623,6 +560,28 @@ set_tp_bases(PyTypeObject *self, PyObject *bases, int
initial)
self->tp_bases = bases;
}
+// Replace tp_bases and tp_base of a type with the world stopped. Returns true
+// on success and false if tp_bases is no longer 'expected'. In the latter
case
+// the caller still owns 'bases' and 'base'.
+static bool
+try_set_tp_bases(PyTypeObject *self, PyObject *expected,
+ PyObject *bases, PyTypeObject *base)
+{
+ ASSERT_TYPE_LOCK_HELD();
+ assert(PyTuple_Check(bases));
+ assert(!(self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN));
+ bool res = false;
+ types_stop_world();
+ if (self->tp_bases == expected) {
+ self->tp_bases = bases;
+ self->tp_base = base;
+ res = true;
+ }
+ types_start_world();
+ ASSERT_TYPE_LOCK_HELD();
+ return res;
+}
+
static inline void
clear_tp_bases(PyTypeObject *self, int final)
{
@@ -651,14 +610,13 @@ lookup_tp_mro(PyTypeObject *self)
}
static inline void
-set_tp_mro(PyTypeObject *self, PyObject *mro, int initial)
+set_tp_mro(PyTypeObject *self, PyObject *mro)
{
if (mro != NULL) {
assert(PyTuple_CheckExact(mro));
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
// XXX tp_mro can probably be statically allocated for each
// static builtin type.
- assert(initial);
assert(self->tp_mro == NULL);
/* Other checks are done via set_tp_bases. */
_Py_SetImmortal(mro);
@@ -667,16 +625,30 @@ set_tp_mro(PyTypeObject *self, PyObject *mro, int initial)
PyUnstable_Object_EnableDeferredRefcount(mro);
}
}
- pinned_mutexes_t pinned;
- if (!initial) {
- type_lock_prevent_release(&pinned);
- types_stop_world();
- }
self->tp_mro = mro;
- if (!initial) {
- types_start_world();
- type_lock_allow_release(&pinned);
+}
+
+// Replace tp_mro of a type with the world stopped. Returns true on success
and
+// false if tp_mro is no longer 'expected'. In the latter case the caller
still
+// owns 'mro'.
+static bool
+try_set_tp_mro(PyTypeObject *self, PyObject *expected, PyObject *mro)
+{
+ ASSERT_TYPE_LOCK_HELD();
+ assert(!(self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN));
+ assert(mro == NULL || PyTuple_CheckExact(mro));
+ bool res = false;
+ types_stop_world();
+ if (self->tp_mro == expected) {
+ if (mro != NULL) {
+ PyUnstable_Object_EnableDeferredRefcount(mro);
+ }
+ self->tp_mro = mro;
+ res = true;
}
+ types_start_world();
+ ASSERT_TYPE_LOCK_HELD();
+ return res;
}
static inline void
@@ -1706,18 +1678,17 @@ type_set_abstractmethods(PyObject *tp, PyObject *value,
void *Py_UNUSED(closure)
return -1;
}
- pinned_mutexes_t pinned;
BEGIN_TYPE_LOCK();
- _PyType_Modified_Unlocked(type);
- type_lock_prevent_release(&pinned);
types_stop_world();
if (abstract)
type_add_flags(type, Py_TPFLAGS_IS_ABSTRACT);
else
type_clear_flags(type, Py_TPFLAGS_IS_ABSTRACT);
types_start_world();
- type_lock_allow_release(&pinned);
+ // Invalidate the version tag after update, while TYPE_LOCK is
+ // still held.
ASSERT_TYPE_LOCK_HELD();
+ _PyType_Modified_Unlocked(type);
END_TYPE_LOCK();
return 0;
@@ -1748,12 +1719,14 @@ type_get_mro(PyObject *tp, void *Py_UNUSED(closure))
static PyTypeObject *find_best_base(PyObject *);
static int mro_internal(PyTypeObject *, int, PyObject **);
static int type_is_subtype_base_chain(PyTypeObject *, PyTypeObject *);
-static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const
char *, int);
+static int compatible_layout(PyTypeObject *, PyTypeObject *, int);
+static void raise_layout_differs(const char *, PyTypeObject *, PyTypeObject *);
+static int update_best_base(PyTypeObject *, PyTypeObject **, PyTypeObject **);
static int add_subclass(PyTypeObject*, PyTypeObject*);
static int add_all_subclasses(PyTypeObject *type, PyObject *bases);
static void remove_subclass(PyTypeObject *, PyTypeObject *);
static void remove_all_subclasses(PyTypeObject *type, PyObject *bases);
-static int update_all_slots(PyTypeObject *);
+static void update_all_slots(PyTypeObject *);
typedef int (*update_callback)(PyTypeObject *, void *);
static int update_subclasses(PyTypeObject *type, PyObject *attr_name,
@@ -1794,8 +1767,13 @@ mro_hierarchy_for_complete_type(PyTypeObject *type,
PyObject *temp)
Py_XDECREF(tuple);
if (res < 0) {
- set_tp_mro(type, old_mro, 0);
- Py_DECREF(new_mro);
+ // Roll back, unless tp_mro has already been replaced again.
+ if (try_set_tp_mro(type, new_mro, old_mro)) {
+ Py_DECREF(new_mro);
+ }
+ else {
+ Py_XDECREF(old_mro);
+ }
return -1;
}
Py_XDECREF(old_mro);
@@ -1831,8 +1809,10 @@ mro_hierarchy_for_complete_type(PyTypeObject *type,
PyObject *temp)
return res;
}
+// Checks on the new __bases__ value that do not depend on mutable type
+// state. Done once per assignment.
static int
-type_check_new_bases(PyTypeObject *type, PyObject *new_bases, PyTypeObject
**best_base)
+type_check_new_bases(PyTypeObject *type, PyObject *new_bases)
{
// Check arguments, this is re-entrant due to the PySys_Audit() call
if (!check_set_special_type_attr(type, new_bases, "__bases__")) {
@@ -1861,7 +1841,42 @@ type_check_new_bases(PyTypeObject *type, PyObject
*new_bases, PyTypeObject **bes
type->tp_name, Py_TYPE(ob)->tp_name);
return -1;
}
- PyTypeObject *base = (PyTypeObject*)ob;
+ PyTypeObject *base = (PyTypeObject *)ob;
+ if (!_PyType_IsReady(base)) {
+ if (PyType_Ready(base) < 0) {
+ return -1;
+ }
+ }
+ if (!_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE)) {
+ PyErr_Format(PyExc_TypeError,
+ "type '%.100s' is not an acceptable base type",
+ base->tp_name);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+typedef enum {
+ NEW_BASES_OK,
+ NEW_BASES_CYCLE,
+ NEW_BASES_LAYOUT_CONFLICT,
+ NEW_BASES_LAYOUT_DIFFERS,
+} new_bases_check_t;
+
+// Checks on the new __bases__ value that depend on the current state of the
+// type hierarchy (inheritance cycles, layout compatibility). This is called
+// with the world stopped and does not raise; see raise_new_bases_error().
+// On success, *best_base is set to the new tp_base (a borrowed reference
+// from new_bases).
+static new_bases_check_t
+type_validate_new_bases(PyTypeObject *type, PyObject *new_bases,
+ PyTypeObject **best_base)
+{
+ assert(types_world_is_stopped());
+ Py_ssize_t n = PyTuple_GET_SIZE(new_bases);
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(new_bases, i);
if (is_subtype_with_mro(lookup_tp_mro(base), base, type) ||
/* In case of reentering here again through a custom mro()
@@ -1876,41 +1891,81 @@ type_check_new_bases(PyTypeObject *type, PyObject
*new_bases, PyTypeObject **bes
(lookup_tp_mro(base) != NULL
&& type_is_subtype_base_chain(base, type)))
{
- PyErr_SetString(PyExc_TypeError,
- "a __bases__ item causes an inheritance cycle");
- return -1;
+ return NEW_BASES_CYCLE;
}
}
- // Compute the new MRO and the new base class
- *best_base = find_best_base(new_bases);
- if (*best_base == NULL)
- return -1;
+ // Compute the new base class. The bases are known to be ready and
+ // acceptable as base types (see type_check_new_bases()).
+ PyTypeObject *winner = NULL;
+ *best_base = NULL;
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(new_bases, i);
+ if (update_best_base(base, best_base, &winner) < 0) {
+ return NEW_BASES_LAYOUT_CONFLICT;
+ }
+ }
- if (!compatible_for_assignment(type, *best_base, "__bases__", 0)) {
- return -1;
+ if (!compatible_layout(type, *best_base, 0)) {
+ return NEW_BASES_LAYOUT_DIFFERS;
}
- return 0;
+ return NEW_BASES_OK;
+}
+
+// Raise the error for a failed type_validate_new_bases() check. 'old_base'
+// is the tp_base of the type when the check was done.
+static void
+raise_new_bases_error(new_bases_check_t check, PyTypeObject *best_base,
+ PyTypeObject *old_base)
+{
+ switch (check) {
+ case NEW_BASES_CYCLE:
+ PyErr_SetString(PyExc_TypeError,
+ "a __bases__ item causes an inheritance cycle");
+ break;
+ case NEW_BASES_LAYOUT_CONFLICT:
+ PyErr_SetString(PyExc_TypeError,
+ "multiple bases have instance lay-out conflict");
+ break;
+ case NEW_BASES_LAYOUT_DIFFERS:
+ raise_layout_differs("__bases__", best_base, old_base);
+ break;
+ default:
+ Py_UNREACHABLE();
+ }
}
+// Returns 0 on success and -1 on error.
static int
-type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject
*best_base)
+type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases)
{
ASSERT_TYPE_LOCK_HELD();
+ // Validate and store the new bases with the world stopped, so that the
+ // hierarchy cannot change in between.
Py_ssize_t n;
+ PyTypeObject *best_base;
+ types_stop_world();
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(&pinned);
- types_stop_world();
- set_tp_bases(type, Py_NewRef(new_bases), 0);
- type->tp_base = (PyTypeObject *)Py_NewRef(best_base);
+ new_bases_check_t check = type_validate_new_bases(type, new_bases,
+ &best_base);
+ if (check == NEW_BASES_OK) {
+ type->tp_bases = Py_NewRef(new_bases);
+ type->tp_base = (PyTypeObject *)Py_NewRef(best_base);
+ }
+ else {
+ // Keep old_base alive for the error message.
+ Py_INCREF(old_base);
+ }
types_start_world();
- type_lock_allow_release(&pinned);
+ if (check != NEW_BASES_OK) {
+ raise_new_bases_error(check, best_base, old_base);
+ Py_DECREF(old_base);
+ return -1;
+ }
PyObject *temp = PyList_New(0);
if (temp == NULL) {
@@ -1934,11 +1989,9 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject
*new_bases, PyTypeObject *b
add to all new_bases */
remove_all_subclasses(type, old_bases);
res = add_all_subclasses(type, new_bases);
- if (update_all_slots(type) < 0) {
- goto bail;
- }
- /* Clear the VALID_VERSION flag of 'type' and all its subclasses. */
- _PyType_Modified_Unlocked(type);
+ /* This also clears the VALID_VERSION flag of 'type' and all its
+ subclasses. */
+ update_all_slots(type);
}
else {
res = 0;
@@ -1960,24 +2013,19 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject
*new_bases, PyTypeObject *b
PyArg_UnpackTuple(PyList_GET_ITEM(temp, i),
"", 2, 3, &cls, &new_mro, &old_mro);
/* Do not rollback if cls has a newer version of MRO. */
- if (lookup_tp_mro(cls) == new_mro) {
- set_tp_mro(cls, Py_XNewRef(old_mro), 0);
+ old_mro = Py_XNewRef(old_mro);
+ if (try_set_tp_mro(cls, new_mro, old_mro)) {
Py_DECREF(new_mro);
}
+ else {
+ Py_XDECREF(old_mro);
+ }
}
Py_DECREF(temp);
bail:
- if (lookup_tp_bases(type) == new_bases) {
- assert(type->tp_base == best_base);
-
- 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(&pinned);
-
+ /* Restore the old bases, unless tp_bases has been replaced again. */
+ if (try_set_tp_bases(type, new_bases, old_bases, old_base)) {
Py_DECREF(new_bases);
Py_DECREF(best_base);
}
@@ -1994,14 +2042,22 @@ static int
type_set_bases(PyObject *tp, PyObject *new_bases, void *Py_UNUSED(closure))
{
PyTypeObject *type = PyTypeObject_CAST(tp);
- PyTypeObject *best_base;
int res;
- BEGIN_TYPE_LOCK();
- res = type_check_new_bases(type, new_bases, &best_base);
- if (res == 0) {
- res = type_set_bases_unlocked(type, new_bases, best_base);
+ if (type_check_new_bases(type, new_bases) < 0) {
+ return -1;
+ }
+ // Create a fresh bases tuple to store. Its identity tells whether
+ // tp_bases was replaced again while TYPE_LOCK was released or due to
+ // re-entrancy from the mro() call.
+ Py_ssize_t n = PyTuple_GET_SIZE(new_bases);
+ PyObject *bases = PyTuple_FromArray(((PyTupleObject *)new_bases)->ob_item,
n);
+ if (bases == NULL) {
+ return -1;
}
+ BEGIN_TYPE_LOCK();
+ res = type_set_bases_unlocked(type, bases);
END_TYPE_LOCK();
+ Py_DECREF(bases);
return res;
}
@@ -3632,25 +3688,45 @@ mro_internal(PyTypeObject *type, int initial, PyObject
**p_old_mro)
ASSERT_TYPE_LOCK_HELD();
PyObject *new_mro, *old_mro;
- int reent;
/* Keep a reference to be able to do a reentrancy check below.
Don't let old_mro be GC'ed and its address be reused for
another object, like (suddenly!) a new tp_mro. */
old_mro = Py_XNewRef(lookup_tp_mro(type));
new_mro = mro_invoke(type); /* might cause reentrance */
- reent = (lookup_tp_mro(type) != old_mro);
- Py_XDECREF(old_mro);
if (new_mro == NULL) {
+ Py_XDECREF(old_mro);
return -1;
}
-
- if (reent) {
+ int res;
+ if (initial) {
+ // The type has not been revealed to other threads yet, so only
+ // same-thread reentrance can have replaced tp_mro.
+ if (lookup_tp_mro(type) == old_mro) {
+ set_tp_mro(type, new_mro);
+ res = 1;
+ }
+ else {
+ res = 0;
+ }
+ }
+ else {
+ // Store with the world stopped. TYPE_LOCK may have been released
+ // while mro_invoke() ran, so check for reentrance inside the
+ // stop-the-world. mro() can itself update types (assign a slot,
+ // for example) and the result is stored anyway, as on the default
+ // build.
+ res = try_set_tp_mro(type, old_mro, new_mro);
+ }
+ if (res == 0) {
+ /* reentrance: tp_mro was replaced by someone else */
Py_DECREF(new_mro);
+ Py_XDECREF(old_mro);
return 0;
}
-
- set_tp_mro(type, new_mro, initial);
+ /* Drop our temporary reference. The reference previously owned by
+ the type is now owned by this function and transferred below. */
+ Py_XDECREF(old_mro);
type_mro_modified(type, new_mro);
/* corner case: the super class might have been hidden
@@ -3675,6 +3751,30 @@ mro_internal(PyTypeObject *type, int initial, PyObject
**p_old_mro)
return 1;
}
+/* Update the best base candidate '*base' (with solid base '*winner') for
+ the next base 'base_i'. Returns -1 if the instance layouts conflict.
+ Does not raise. */
+static int
+update_best_base(PyTypeObject *base_i, PyTypeObject **base,
+ PyTypeObject **winner)
+{
+ PyTypeObject *candidate = solid_base(base_i);
+ if (*winner == NULL) {
+ *winner = candidate;
+ *base = base_i;
+ }
+ else if (PyType_IsSubtype(*winner, candidate))
+ ;
+ else if (PyType_IsSubtype(candidate, *winner)) {
+ *winner = candidate;
+ *base = base_i;
+ }
+ else {
+ return -1;
+ }
+ return 0;
+}
+
/* Calculate the best base amongst multiple base classes.
This is the first one that's on the path to the "solid base". */
@@ -3682,7 +3782,7 @@ static PyTypeObject *
find_best_base(PyObject *bases)
{
Py_ssize_t i, n;
- PyTypeObject *base, *winner, *candidate;
+ PyTypeObject *base, *winner;
assert(PyTuple_Check(bases));
n = PyTuple_GET_SIZE(bases);
@@ -3709,18 +3809,7 @@ find_best_base(PyObject *bases)
base_i->tp_name);
return NULL;
}
- candidate = solid_base(base_i);
- if (winner == NULL) {
- winner = candidate;
- base = base_i;
- }
- else if (PyType_IsSubtype(winner, candidate))
- ;
- else if (PyType_IsSubtype(candidate, winner)) {
- winner = candidate;
- base = base_i;
- }
- else {
+ if (update_best_base(base_i, &base, &winner) < 0) {
PyErr_SetString(
PyExc_TypeError,
"multiple bases have "
@@ -3761,169 +3850,28 @@ solid_base(PyTypeObject *type)
}
}
-#ifdef Py_GIL_DISABLED
-
-// The structures and functions below are used in the free-threaded build
-// to safely make updates to type slots, on type_setattro() for a slot
-// or when __bases__ is re-assigned. Since the slots are read without atomic
-// operations and without locking, we can only safely update them while the
-// world is stopped. However, with the world stopped, we are very limited on
-// which APIs can be safely used. For example, calling _PyObject_HashDictKey()
-// or _PyDict_GetItemRef_KnownHash() are not safe and can potentially cause
-// deadlocks. Hashing can be re-entrant and _PyDict_GetItemRef_KnownHash can
-// acquire a lock if the dictionary is not owned by the current thread, to
-// mark it shared on reading.
-//
-// We do the slot updates in two steps. First, with TYPE_LOCK held, we lookup
-// the descriptor for each slot, for each subclass. We build a queue of
-// updates to perform but don't actually update the type structures. After we
-// are finished the lookups, we stop-the-world and apply all of the updates.
-// The apply_slot_updates() code is simple and easy to confirm that it is
-// safe.
-
-typedef struct {
- PyTypeObject *type;
- void **slot_ptr;
- void *slot_value;
-} slot_update_item_t;
-
-// The number of slot updates performed is based on the number of changed
-// slots and the number of subclasses. It's possible there are many updates
-// required if there are many subclasses (potentially an unbounded amount).
-// Usually the number of slot updates is small, most often zero or one. When
-// running the unit tests, we don't exceed 20. The chunk size is set to
-// handle the common case with a single chunk and to not require too many
-// chunk allocations if there are many subclasses.
-#define SLOT_UPDATE_CHUNK_SIZE 30
-
-typedef struct _slot_update {
- struct _slot_update *prev;
- Py_ssize_t n;
- slot_update_item_t updates[SLOT_UPDATE_CHUNK_SIZE];
-} slot_update_chunk_t;
-
-// a queue of updates to be performed
-typedef struct {
- slot_update_chunk_t *head;
-} slot_update_t;
-
-static slot_update_chunk_t *
-slot_update_new_chunk(void)
-{
- slot_update_chunk_t *chunk = PyMem_Malloc(sizeof(slot_update_chunk_t));
- if (chunk == NULL) {
- PyErr_NoMemory();
- return NULL;
- }
- chunk->prev = NULL;
- chunk->n = 0;
- return chunk;
-}
-
-static void
-slot_update_free_chunks(slot_update_t *updates)
-{
- slot_update_chunk_t *chunk = updates->head;
- while (chunk != NULL) {
- slot_update_chunk_t *prev = chunk->prev;
- PyMem_Free(chunk);
- chunk = prev;
- }
-}
-
-static int
-queue_slot_update(slot_update_t *updates, PyTypeObject *type,
- void **slot_ptr, void *slot_value)
-{
- if (*slot_ptr == slot_value) {
- return 0; // slot pointer not actually changed, don't queue update
- }
- if (updates->head == NULL || updates->head->n == SLOT_UPDATE_CHUNK_SIZE) {
- slot_update_chunk_t *chunk = slot_update_new_chunk();
- if (chunk == NULL) {
- return -1; // out-of-memory
- }
- chunk->prev = updates->head;
- updates->head = chunk;
- }
- slot_update_item_t *item = &updates->head->updates[updates->head->n];
- item->type = type;
- item->slot_ptr = slot_ptr;
- item->slot_value = slot_value;
- updates->head->n++;
- assert(updates->head->n <= SLOT_UPDATE_CHUNK_SIZE);
- return 0;
-}
-
-static void
-apply_slot_updates(slot_update_t *updates)
-{
- assert(types_world_is_stopped());
- slot_update_chunk_t *chunk = updates->head;
- while (chunk != NULL) {
- for (Py_ssize_t i = 0; i < chunk->n; i++) {
- slot_update_item_t *item = &chunk->updates[i];
- *(item->slot_ptr) = item->slot_value;
- if (item->slot_value == slot_tp_call) {
- /* A generic __call__ is incompatible with vectorcall */
- type_clear_flags(item->type, Py_TPFLAGS_HAVE_VECTORCALL);
- }
- }
- chunk = chunk->prev;
- }
-}
-
-static void
-apply_type_slot_updates(slot_update_t *updates)
-{
- // This must be done carefully to avoid data races and deadlocks. We
- // have just updated the type __dict__, while holding TYPE_LOCK. We have
- // collected all of the required type slot updates into the 'updates'
- // queue. Note that those updates can apply to multiple types since
- // subclasses might also be affected by the dict change.
- //
- // We need to prevent other threads from writing to the dict before we can
- // finish updating the slots. The actual stores to the slots are done
- // with the world stopped. If we block on the stop-the-world mutex then
- // we could release TYPE_LOCK mutex and potentially allow other threads
- // to update the dict. That's because TYPE_LOCK was acquired using a
- // critical section.
- //
- // 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(&pinned);
-}
-
-#else
-
-// dummy definition, this parameter is only NULL in the default build
-typedef void slot_update_t;
-
-#endif
+// In the free-threaded build, type slots and tp_flags are read without
+// atomics and without locking, so they can only be updated with the world
+// stopped (or before the type is revealed to other threads). When a slot
+// name is assigned with type_setattro() or __bases__ is re-assigned, the
+// descriptor lookups for the type and its subclasses and the slot stores are
+// all done within one stop-the-world. The new slots therefore agree with
+// the type dicts as they are when the world is stopped, whatever happened
+// while TYPE_LOCK was released waiting for it. With the world stopped, no
+// other thread runs, so dicts and the subclass lists can be read without
+// locking and with borrowed references. Nothing that can run user code,
+// block or deallocate is allowed; see find_slot_name_in_mro() and
+// recurse_down_subclasses().
/// data passed to update_slots_callback()
typedef struct {
- slot_update_t *queued_updates;
pytype_slotdef **defs;
} update_callback_data_t;
static void object_dealloc(PyObject *);
static PyObject *object_new(PyTypeObject *, PyObject *, PyObject *);
static int object_init(PyObject *, PyObject *, PyObject *);
-static int update_slot(PyTypeObject *, PyObject *, slot_update_t *update);
+static int update_slot(PyTypeObject *, PyObject *);
static void fixup_slot_dispatchers(PyTypeObject *);
static int type_ready(PyTypeObject *, int, int);
static int type_ready_publish(PyTypeObject *, int);
@@ -4424,7 +4372,7 @@ type_new_alloc(type_new_ctx *ctx)
type->tp_as_mapping = &et->as_mapping;
type->tp_as_buffer = &et->as_buffer;
- set_tp_bases(type, Py_NewRef(ctx->bases), 1);
+ set_tp_bases(type, Py_NewRef(ctx->bases));
type->tp_base = (PyTypeObject *)Py_NewRef(ctx->base);
type->tp_dealloc = subtype_dealloc;
@@ -4889,10 +4837,72 @@ type_new_get_slots(type_new_ctx *ctx, PyObject *dict)
}
+/* Copy the class namespace. Keys that are str subclasses are replaced by
+ exact str copies so that the type dict only holds exact str keys (see
+ type_setattro() which does the same for assigned names). Lookups in a
+ dict with exact str keys cannot run user __eq__ methods, which the slot
+ update code relies on. Non-str keys are left alone; type_new_impl()
+ warns about them. */
+static PyObject *
+type_new_copy_dict(PyObject *orig_dict)
+{
+ PyObject *dict = _PyDict_CopyAsDict(orig_dict);
+ if (dict == NULL) {
+ return NULL;
+ }
+ if (((PyDictObject *)dict)->ma_keys->dk_kind != DICT_KEYS_GENERAL) {
+ // Only exact str keys are possible, nothing to do.
+ return dict;
+ }
+ Py_ssize_t pos = 0;
+ PyObject *key, *value;
+ int rebuild = 0;
+ while (PyDict_Next(dict, &pos, &key, &value)) {
+ if (PyUnicode_Check(key) && !PyUnicode_CheckExact(key)) {
+ rebuild = 1;
+ break;
+ }
+ }
+ if (!rebuild) {
+ return dict;
+ }
+ // Rebuild rather than delete/insert so that the key order is kept.
+ PyObject *newdict = PyDict_New();
+ if (newdict == NULL) {
+ Py_DECREF(dict);
+ return NULL;
+ }
+ pos = 0;
+ while (PyDict_Next(dict, &pos, &key, &value)) {
+ PyObject *newkey;
+ if (PyUnicode_Check(key) && !PyUnicode_CheckExact(key)) {
+ newkey = _PyUnicode_Copy(key);
+ if (newkey == NULL) {
+ goto error;
+ }
+ }
+ else {
+ newkey = Py_NewRef(key);
+ }
+ int err = PyDict_SetItem(newdict, newkey, value);
+ Py_DECREF(newkey);
+ if (err < 0) {
+ goto error;
+ }
+ }
+ Py_DECREF(dict);
+ return newdict;
+
+error:
+ Py_DECREF(dict);
+ Py_DECREF(newdict);
+ return NULL;
+}
+
static PyTypeObject*
type_new_init(type_new_ctx *ctx)
{
- PyObject *dict = _PyDict_CopyAsDict(ctx->orig_dict);
+ PyObject *dict = type_new_copy_dict(ctx->orig_dict);
if (dict == NULL) {
goto error;
}
@@ -5630,7 +5640,7 @@ type_from_slots_or_spec(
/* Set slots we have prepared */
type->tp_base = (PyTypeObject *)Py_NewRef(base);
- set_tp_bases(type, bases, 1);
+ set_tp_bases(type, bases);
bases = NULL; // We give our reference to bases to the type
type->tp_doc = tp_doc;
@@ -6177,6 +6187,52 @@ find_name_in_mro(PyTypeObject *type, PyObject *name,
_PyStackRef *out)
return res;
}
+#ifdef Py_GIL_DISABLED
+/* Variant of find_name_in_mro() used to compute slots. 'name' must be an
+ exact str. Keys of the MRO dicts that are not exact str are skipped
+ without being compared, so no user code runs. This can be called with the
+ world stopped; the result is then a borrowed reference since no other
+ thread can drop it. Returns 0 if the name was not found and 1 if it was. */
+static int
+find_slot_name_in_mro(PyTypeObject *type, PyObject *name, _PyStackRef *out)
+{
+ assert(PyUnicode_CheckExact(name));
+ *out = PyStackRef_NULL;
+ PyObject *mro = lookup_tp_mro(type);
+ if (mro == NULL) {
+ return 0;
+ }
+ int res = 0;
+ int world_stopped = types_world_is_stopped();
+ if (!world_stopped) {
+ // tp_mro can be replaced while the dict locks are released.
+ Py_INCREF(mro);
+ }
+ Py_ssize_t n = PyTuple_GET_SIZE(mro);
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *base = PyTuple_GET_ITEM(mro, i);
+ PyDictObject *dict = (PyDictObject
*)lookup_tp_dict(_PyType_CAST(base));
+ assert(dict && PyDict_Check(dict));
+ if (world_stopped) {
+ PyObject *value = _PyDict_GetItem_ExactUnicode(dict, name);
+ if (value != NULL) {
+ *out = PyStackRef_FromPyObjectBorrow(value);
+ res = 1;
+ break;
+ }
+ }
+ else if (_PyDict_GetItemStackRef_ExactUnicode(dict, name, out)) {
+ res = 1;
+ break;
+ }
+ }
+ if (!world_stopped) {
+ Py_DECREF(mro);
+ }
+ return res;
+}
+#endif
+
/* Check if the "readied" PyUnicode name
is a double-underscore special name. */
static int
@@ -6397,24 +6453,19 @@ void
_PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned
long flags)
{
BEGIN_TYPE_LOCK();
- /* Ideally, changing flags and invalidating the old version tag would
- happen in one step. But _PyType_Modified_Unlocked() is re-entrant and
- cannot run with the world stopped, so we must invalidate first.
- Immutable/static-builtin types are skipped because
- set_flags_recursive() does not modify them. */
- if (!PyType_HasFeature(self, Py_TPFLAGS_IMMUTABLETYPE) &&
- (self->tp_flags & mask) != flags)
- {
- _PyType_Modified_Unlocked(self);
- }
- /* Keep TYPE_LOCK held while waiting for stop-the-world so no thread
- can reassign a version tag before the flag update. */
- pinned_mutexes_t pinned;
- type_lock_prevent_release(&pinned);
+ // Immutable/static-builtin types are skipped because
+ // set_flags_recursive() does not modify them.
+ int modified = (!PyType_HasFeature(self, Py_TPFLAGS_IMMUTABLETYPE) &&
+ (self->tp_flags & mask) != flags);
types_stop_world();
set_flags_recursive(self, mask, flags);
types_start_world();
- type_lock_allow_release(&pinned);
+ // Invalidate the version tags after update, while TYPE_LOCK is
+ // still held.
+ ASSERT_TYPE_LOCK_HELD();
+ if (modified) {
+ _PyType_Modified_Unlocked(self);
+ }
END_TYPE_LOCK();
}
@@ -6589,9 +6640,11 @@ type_update_dict(PyTypeObject *type, PyDictObject *dict,
PyObject *name,
// We don't want any re-entrancy between when we update the dict
// and call _PyType_Modified_Unlocked, including running the destructor
// of the current value as it can observe the cache in an inconsistent
- // state. Because we have an exact unicode and our dict has exact
- // unicodes we know that this will all complete without releasing
- // the locks.
+ // state. The name is an exact unicode (see type_setattro()) and
+ // str subclass keys are copied to exact str by type_new_copy_dict()
+ // and type_setattro(). The type dict can still hold non-str keys
+ // (type_new_impl() only warns about them) and comparing with one of
+ // those can run user code that releases the locks.
if (_PyDict_GetItemRef_Unicode_LockHeld(dict, name, old_value) < 0) {
return -1;
}
@@ -6614,32 +6667,20 @@ type_update_dict(PyTypeObject *type, PyDictObject
*dict, PyObject *name,
return 0;
}
-static int
+static void
update_slot_after_setattr(PyTypeObject *type, PyObject *name)
{
+ types_stop_world();
+ update_slot(type, name);
+ types_start_world();
#ifdef Py_GIL_DISABLED
- assert(SLOT_UPDATE_CHUNK_SIZE >= MAX_EQUIV);
- slot_update_chunk_t chunk = {0};
- // Stack allocate the first chunk. It is usually the only one needed but
- // updates are queued for subclasses as well, so more chunks are needed if
- // the type has more than SLOT_UPDATE_CHUNK_SIZE subclasses.
- slot_update_t queued_updates = {&chunk};
-
- int res = update_slot(type, name, &queued_updates);
- if (res == 0 && queued_updates.head->n > 0) {
- apply_type_slot_updates(&queued_updates);
- ASSERT_TYPE_LOCK_HELD();
- }
- slot_update_chunk_t *cur = queued_updates.head;
- while (cur != &chunk) {
- slot_update_chunk_t *prev = cur->prev;
- PyMem_Free(cur);
- cur = prev;
- }
- return res;
-#else
- update_slot(type, name, NULL);
- return 0;
+ // Invalidate again, after the slots are updated and while TYPE_LOCK is
+ // still held. Other threads may have assigned new version tags while
+ // the lock was released waiting for the stop-the-world and cached
+ // decisions based on the old slots. This is a no-op if the tags are
+ // still invalid.
+ ASSERT_TYPE_LOCK_HELD();
+ _PyType_Modified_Unlocked(type);
#endif
}
@@ -6729,7 +6770,7 @@ type_setattro(PyObject *self, PyObject *name, PyObject
*value)
if (res == 0) {
if (is_dunder_name(name) && has_slotdef(name)) {
// The name corresponds to a type slot.
- res = update_slot_after_setattr(type, name);
+ update_slot_after_setattr(type, name);
}
}
END_TYPE_DICT_LOCK();
@@ -7488,6 +7529,24 @@ compatible_with_tp_base(PyTypeObject *child)
child->tp_dealloc == parent->tp_dealloc));
}
+// Compare two ht_slots tuples by string content, without calling a user
+// __eq__, since this can be called with the world stopped.
+static int
+same_slot_names(PyObject *slots_a, PyObject *slots_b)
+{
+ Py_ssize_t n = PyTuple_GET_SIZE(slots_a);
+ if (n != PyTuple_GET_SIZE(slots_b)) {
+ return 0;
+ }
+ for (Py_ssize_t i = 0; i < n; i++) {
+ if (!_PyUnicode_Equal(PyTuple_GET_ITEM(slots_a, i),
+ PyTuple_GET_ITEM(slots_b, i))) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
static int
same_slots_added(PyTypeObject *a, PyTypeObject *b)
{
@@ -7510,7 +7569,7 @@ same_slots_added(PyTypeObject *a, PyTypeObject *b)
slots_a = ((PyHeapTypeObject *)a)->ht_slots;
slots_b = ((PyHeapTypeObject *)b)->ht_slots;
if (slots_a && slots_b) {
- if (PyObject_RichCompareBool(slots_a, slots_b, Py_EQ) != 1)
+ if (!same_slot_names(slots_a, slots_b))
return 0;
size += sizeof(PyObject *) * PyTuple_GET_SIZE(slots_a);
}
@@ -7528,28 +7587,21 @@ compatible_flags(int setclass, PyTypeObject *origto,
PyTypeObject *newto, unsign
: !(~(origto->tp_flags & flags) & (newto->tp_flags &
flags));
}
+// Return 1 if the object layout of 'newto' is compatible with 'origto' (for
+// __class__ assignment) or with the base of 'origto' (for __bases__
+// assignment), else 0. Does not raise and does not run user code.
static int
-compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto,
- const char *attr, int setclass)
+compatible_layout(PyTypeObject *origto, PyTypeObject *newto, int setclass)
{
PyTypeObject *newbase, *oldbase;
PyTypeObject *oldto = setclass ? origto : origto->tp_base;
- if (setclass && newto->tp_free != oldto->tp_free) {
- PyErr_Format(PyExc_TypeError,
- "%s assignment: "
- "'%s' deallocator differs from '%s'",
- attr,
- newto->tp_name,
- oldto->tp_name);
- return 0;
- }
if (!compatible_flags(setclass, origto, newto,
Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_INLINE_VALUES |
Py_TPFLAGS_PREHEADER))
{
- goto differs;
+ return 0;
}
/* For __class__ assignment, tp_dictoffset and tp_weaklistoffset should
be the same for old and new types.
@@ -7559,12 +7611,12 @@ compatible_for_assignment(PyTypeObject *origto,
PyTypeObject *newto,
if ((setclass || newto->tp_dictoffset)
&& origto->tp_dictoffset != newto->tp_dictoffset)
{
- goto differs;
+ return 0;
}
if ((setclass || newto->tp_weaklistoffset)
&& origto->tp_weaklistoffset != newto->tp_weaklistoffset)
{
- goto differs;
+ return 0;
}
/*
It's tricky to tell if two arbitrary types are sufficiently compatible as
@@ -7585,17 +7637,43 @@ compatible_for_assignment(PyTypeObject *origto,
PyTypeObject *newto,
if (newbase != oldbase &&
(newbase->tp_base != oldbase->tp_base ||
!same_slots_added(newbase, oldbase))) {
- goto differs;
+ return 0;
}
return 1;
-differs:
+}
+
+static void
+raise_layout_differs(const char *attr, PyTypeObject *newto,
+ PyTypeObject *oldto)
+{
PyErr_Format(PyExc_TypeError,
"%s assignment: "
"'%s' object layout differs from '%s'",
attr,
newto->tp_name,
oldto->tp_name);
- return 0;
+}
+
+static int
+compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto,
+ const char *attr, int setclass)
+{
+ PyTypeObject *oldto = setclass ? origto : origto->tp_base;
+
+ if (setclass && newto->tp_free != oldto->tp_free) {
+ PyErr_Format(PyExc_TypeError,
+ "%s assignment: "
+ "'%s' deallocator differs from '%s'",
+ attr,
+ newto->tp_name,
+ oldto->tp_name);
+ return 0;
+ }
+ if (!compatible_layout(origto, newto, setclass)) {
+ raise_layout_differs(attr, newto, oldto);
+ return 0;
+ }
+ return 1;
}
@@ -8715,10 +8793,23 @@ overrides_hash(PyTypeObject *type)
PyObject *dict = lookup_tp_dict(type);
assert(dict != NULL);
+#ifdef Py_GIL_DISABLED
+ // Ignore non-str keys, consistent with find_slot_name_in_mro().
+ _PyStackRef ref;
+ int r = _PyDict_GetItemStackRef_ExactUnicode(
+ (PyDictObject *)dict, &_Py_ID(__eq__), &ref);
+ PyStackRef_XCLOSE(ref);
+ if (r == 0) {
+ r = _PyDict_GetItemStackRef_ExactUnicode(
+ (PyDictObject *)dict, &_Py_ID(__hash__), &ref);
+ PyStackRef_XCLOSE(ref);
+ }
+#else
int r = PyDict_Contains(dict, &_Py_ID(__eq__));
if (r == 0) {
r = PyDict_Contains(dict, &_Py_ID(__hash__));
}
+#endif
return r;
}
@@ -9023,7 +9114,7 @@ type_ready_set_bases(PyTypeObject *type, int initial)
if (bases == NULL) {
return -1;
}
- set_tp_bases(type, bases, 1);
+ set_tp_bases(type, bases);
}
return 0;
}
@@ -11853,7 +11944,9 @@ clear_slot_descrs(PyObject **descrs)
}
/* Scan the dicts along the MRO once and set descrs[i] to a strong reference
- to what find_name_in_mro() would return for slotdefs[i].name, or NULL.
+ to what find_name_in_mro() (find_slot_name_in_mro() in the free-threaded
+ build) would return for slotdefs[i].name, or NULL. Dicts with keys that
+ are not exact str are not scanned.
Return -1 (descrs[] all NULL) if some dict cannot be scanned. */
static int
collect_slot_descrs(PyTypeObject *type, PyObject **descrs)
@@ -11971,17 +12064,18 @@ collect_slot_descrs(PyTypeObject *type, PyObject
**descrs)
* When done, next_p is set to the next slotdef with a different offset,
* because that's convenient for fixup_slot_dispatchers().
*
- * If the queued_updates pointer is provided, the actual updates to the slot
- * pointers are queued, rather than being immediately performed. That argument
- * is only used for the free-threaded build since those updates need to be
- * done while the world is stopped.
+ * In the free-threaded build, this is called with the world stopped for a
+ * type that has been revealed to other threads. The descriptors are looked
+ * up with find_slot_name_in_mro(), which ignores non-str dict keys.
*
- * This function will only return an error if the queued_updates argument is
- * provided and allocating memory for the queue fails. Other exceptions that
- * occur internally are ignored, such as when looking up descriptors. */
-static int
+ * If descrs is not NULL, it holds the descriptors found by
+ * collect_slot_descrs() and no lookups are done.
+ *
+ * Exceptions that occur internally are ignored, such as when looking up
+ * descriptors. */
+static void
update_one_slot(PyTypeObject *type, pytype_slotdef *p, pytype_slotdef **next_p,
- slot_update_t *queued_updates, PyObject **descrs)
+ PyObject **descrs)
{
ASSERT_NEW_TYPE_OR_LOCKED(type);
@@ -12008,7 +12102,7 @@ update_one_slot(PyTypeObject *type, pytype_slotdef *p,
pytype_slotdef **next_p,
if (next_p != NULL) {
*next_p = p;
}
- return 0;
+ return;
}
/* We may end up clearing live exceptions below, so make sure it's ours. */
assert(!PyErr_Occurred());
@@ -12022,8 +12116,12 @@ update_one_slot(PyTypeObject *type, pytype_slotdef *p,
pytype_slotdef **next_p,
descr_ref = res ? PyStackRef_FromPyObjectBorrow(found) :
PyStackRef_NULL;
}
else {
+#ifdef Py_GIL_DISABLED
+ res = find_slot_name_in_mro(type, p->name_strobj, &descr_ref);
+#else
/* Use faster uncached lookup as we won't get any cache hits
during type setup. */
res = find_name_in_mro(type, p->name_strobj, &descr_ref);
+#endif
}
if (res <= 0) {
if (ptr == (void**)&type->tp_iternext) {
@@ -12115,9 +12213,7 @@ update_one_slot(PyTypeObject *type, pytype_slotdef *p,
pytype_slotdef **next_p,
}
if (p->function == slot_tp_call) {
/* A generic __call__ is incompatible with vectorcall */
- if (queued_updates == NULL) {
- type_clear_flags(type, Py_TPFLAGS_HAVE_VECTORCALL);
- }
+ type_clear_flags(type, Py_TPFLAGS_HAVE_VECTORCALL);
}
}
PyStackRef_CLOSE(descr_ref);
@@ -12130,26 +12226,12 @@ update_one_slot(PyTypeObject *type, pytype_slotdef
*p, pytype_slotdef **next_p,
slot_value = generic;
}
-#ifdef Py_GIL_DISABLED
- if (queued_updates != NULL) {
- // queue the update to perform later, while world is stopped
- if (queue_slot_update(queued_updates, type, ptr, slot_value) < 0) {
- return -1;
- }
- } else {
- // do the update to the type structure now
- *ptr = slot_value;
- }
-#else
- // always do the update immediately
- assert(queued_updates == NULL);
+ ASSERT_WORLD_STOPPED_OR_NEW_TYPE(type);
*ptr = slot_value;
-#endif
if (next_p != NULL) {
*next_p = p;
}
- return 0;
}
/* In the type, update the slots whose slotdefs are gathered in the pp array.
@@ -12162,17 +12244,15 @@ update_slots_callback(PyTypeObject *type, void *data)
update_callback_data_t *update_data = (update_callback_data_t *)data;
pytype_slotdef **pp = update_data->defs;
for (; *pp; pp++) {
- if (update_one_slot(type, *pp, NULL, update_data->queued_updates,
- NULL) < 0) {
- return -1;
- }
+ update_one_slot(type, *pp, NULL, NULL);
}
return 0;
}
-/* Update the slots after assignment to a class (type) attribute. */
+/* Update the slots after assignment to a class (type) attribute. In the
+ free-threaded build, this is called with the world stopped. */
static int
-update_slot(PyTypeObject *type, PyObject *name, slot_update_t *queued_updates)
+update_slot(PyTypeObject *type, PyObject *name)
{
pytype_slotdef *ptrs[MAX_EQUIV];
pytype_slotdef *p;
@@ -12201,7 +12281,6 @@ update_slot(PyTypeObject *type, PyObject *name,
slot_update_t *queued_updates)
update_callback_data_t callback_data;
callback_data.defs = ptrs;
- callback_data.queued_updates = queued_updates;
return update_subclasses(type, name,
update_slots_callback, (void *)&callback_data);
}
@@ -12222,60 +12301,29 @@ fixup_slot_dispatchers(PyTypeObject *type)
descrs = NULL;
}
for (pytype_slotdef *p = slotdefs; p->name; ) {
- int rv = update_one_slot(type, p, &p, NULL, descrs);
- // always returns 0 if queued_updates == NULL
- assert (rv == 0);
- (void)rv;
+ update_one_slot(type, p, &p, descrs);
}
if (descrs != NULL) {
clear_slot_descrs(descrs);
}
}
-#ifdef Py_GIL_DISABLED
-
-// Called when __bases__ is re-assigned.
-static int
+// Called when __bases__ is re-assigned. Also clears the VALID_VERSION
+// flag of 'type' and all its subclasses.
+static void
update_all_slots(PyTypeObject* type)
{
- // Note that update_slot() can fail due to out-of-memory when allocating
- // the queue chunks to hold the updates. That's unlikely since the number
- // of updates is normally small but we handle that case. update_slot()
- // can fail internally for other reasons (a lookup fails) but those
- // errors are suppressed.
- slot_update_t queued_updates = {0};
+ types_stop_world();
for (pytype_slotdef *p = slotdefs; p->name; p++) {
- if (update_slot(type, p->name_strobj, &queued_updates) < 0) {
- if (queued_updates.head) {
- slot_update_free_chunks(&queued_updates);
- }
- return -1;
- }
- }
- if (queued_updates.head != NULL) {
- apply_type_slot_updates(&queued_updates);
- ASSERT_TYPE_LOCK_HELD();
- slot_update_free_chunks(&queued_updates);
- }
- return 0;
-}
-
-#else
-
-// Called when __bases__ is re-assigned.
-static int
-update_all_slots(PyTypeObject* type)
-{
- pytype_slotdef *p;
- for (p = slotdefs; p->name; p++) {
/* update_slot returns int but can't actually fail in this case*/
- update_slot(type, p->name_strobj, NULL);
+ update_slot(type, p->name_strobj);
}
- return 0;
+ types_start_world();
+ // Invalidate the version tags after the slots are updated, while
+ // TYPE_LOCK is still held: see update_slot_after_setattr().
+ _PyType_Modified_Unlocked(type);
}
-#endif
-
int
_PyType_InitSlotDefs(PyInterpreterState *interp)
{
@@ -12432,6 +12480,31 @@ recurse_down_subclasses(PyTypeObject *type, PyObject
*attr_name,
Py_ssize_t i = 0;
PyObject *ref;
while (PyDict_Next(subclasses, &i, NULL, &ref)) {
+#ifdef Py_GIL_DISABLED
+ // With the world stopped, use borrowed references to the subclasses,
+ // so that dropping a reference cannot run type_dealloc(). Look up
+ // attr_name without calling __eq__ of non-str keys, consistent with
+ // find_slot_name_in_mro().
+ assert(types_world_is_stopped());
+ if (_PyWeakref_IS_DEAD(ref)) {
+ continue;
+ }
+ PyTypeObject *subclass =
+ _PyType_CAST(((PyWeakReference *)ref)->wr_object);
+
+ /* Avoid recursing down into unaffected classes */
+ PyObject *dict = lookup_tp_dict(subclass);
+ if (dict != NULL && PyDict_Check(dict) &&
+ _PyDict_GetItem_ExactUnicode((PyDictObject *)dict,
+ attr_name) != NULL)
+ {
+ continue;
+ }
+
+ if (update_subclasses(subclass, attr_name, callback, data) < 0) {
+ return -1;
+ }
+#else
PyTypeObject *subclass = type_from_ref(ref);
if (subclass == NULL) {
continue;
@@ -12456,6 +12529,7 @@ recurse_down_subclasses(PyTypeObject *type, PyObject
*attr_name,
return -1;
}
Py_DECREF(subclass);
+#endif
}
return 0;
}
_______________________________________________
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]