Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
672c9a1
gh-158195: Do not hold the type lock during stop-the-world
nascheme Oct 20, 2026
45823fa
Replace int pointer with int and bool option.
nascheme Oct 20, 2026
1951c50
Strengthen type-lock regression tests and reduce their cost
nascheme Oct 21, 2026
167ed18
Enable deferred refcounting only on the stored MRO
nascheme Oct 21, 2026
be1b1fd
Store a private copy of the assigned __bases__ tuple
nascheme Oct 21, 2026
8ca6ee8
Invalidate type version tags before dropping queue references
nascheme Oct 21, 2026
5f92ab0
Cleanup try_set_tp_bases() and try_set_tp_mro()
nascheme Oct 21, 2026
7917d6a
Correct the comment on type_validate_new_bases()
nascheme Oct 21, 2026
2aca6d5
Minor comments cleanup, reduce.
nascheme Oct 21, 2026
c7ac4e7
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Oct 21, 2026
8b8a87c
Avoid looping if mro() assigns a slot.
nascheme Oct 23, 2026
04afc4d
Copy str subclass keys of the class namespace to exact str.
nascheme Oct 24, 2026
8770238
Do type slot and __bases__ updates in a single stop-the-world.
nascheme Oct 24, 2026
5687320
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Oct 28, 2026
b5fab45
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Oct 28, 2026
1fc90df
Address review suggestions
nascheme Oct 30, 2026
d06b4af
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Oct 30, 2026
a26384d
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Nov 1, 2026
4f369b3
Merge remote-tracking branch 'origin/main' into gh-158195-unpin-type-…
nascheme Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_free_threading/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
174 changes: 171 additions & 3 deletions Lib/test/test_free_threading/test_type.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import gc
import threading
import unittest

Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 38 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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},
Expand Down
Loading
Loading