Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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__())


if __name__ == "__main__":
unittest.main()
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 @@ -3348,6 +3385,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