Skip to content
Draft
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
6 changes: 6 additions & 0 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ 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);
extern 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);
#ifdef Py_GIL_DISABLED
extern PyObject *_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key);
extern int _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp,
PyObject *key,
_PyStackRef *value_addr);
#endif
extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value);

extern int _PyDict_Pop_KnownHash(
Expand Down
106 changes: 106 additions & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4099,6 +4099,44 @@ def test_pow_wrapper_error_messages(self):
'expected 1 or 2 arguments, got 3',
int().__rpow__, 1, 2, 3)

@support.cpython_only
def test_assign_bases_tuple_identity(self):
class A:
pass

class B(A):
pass

bases = (A,)
B.__bases__ = bases
self.assertEqual(B.__bases__, bases)
if support.Py_GIL_DISABLED:
self.assertIsNot(B.__bases__, bases)
else:
self.assertIs(B.__bases__, bases)

def test_setattr_slot_update_error(self):
# An error raised while updating the slots of subclasses must be
# propagated by the attribute assignment.
class K:
def __hash__(self):
return hash('__repr__')
def __eq__(self, other):
raise ZeroDivisionError

class A:
pass

with self.assertWarns(RuntimeWarning):
B = type('B', (A,), {K(): 1})
if support.Py_GIL_DISABLED:
# The slot update skips non-str keys without comparing them.
A.__repr__ = lambda self: 'A'
else:
with self.assertRaises(ZeroDivisionError):
A.__repr__ = lambda self: 'A'
self.assertEqual(repr(A()), 'A')

def test_mutable_bases(self):
# Testing mutable bases...

Expand Down Expand Up @@ -4164,6 +4202,33 @@ class E(D):
else:
self.fail("shouldn't be able to create inheritance cycles")

def test_assign_bases_many_subclasses(self):
# Reassigning bases must update slots in all affected subclasses.
class A:
x = 'hello'
def __call__(self):
return 123
def __getitem__(self, index):
return None

class X:
x = 'bye'

class B(A):
pass

subclasses = []
for i in range(1000):
sc = type(f'Sub{i}', (B,), {})
subclasses.append(sc)

self.assertEqual(subclasses[0]()(), 123)
self.assertEqual(subclasses[0]().x, 'hello')
B.__bases__ = (X,)
with self.assertRaises(TypeError):
subclasses[0]()()
self.assertEqual(subclasses[0]().x, 'bye')

def test_builtin_bases(self):
# Make sure all the builtin types can have their base queried without
# segfaulting. See issue #5787.
Expand Down Expand Up @@ -4875,6 +4940,47 @@ def __new__(mcls, name, bases, ns):
with self.assertWarnsRegex(RuntimeWarning, 'MyClass'):
MyClass = meta('MyClass', (), {})

@support.cpython_only
def test_str_subclass_namespace_key_identity(self):
class Key(str):
pass

key = Key('other')
C = type('C', (), {key: 1})
stored_key = next(iter(C.__dict__))
self.assertEqual(stored_key, key)
self.assertEqual(C.other, 1)
if support.Py_GIL_DISABLED:
self.assertIs(type(stored_key), str)
else:
self.assertIs(stored_key, key)

@unittest.skipUnless(support.Py_GIL_DISABLED, 'needs a free-threaded build')
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
26 changes: 26 additions & 0 deletions Lib/test/test_free_threading/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,31 @@ def mutate_thread():
threading_helper.run_concurrently([repr_thread, 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)


if __name__ == "__main__":
unittest.main()
193 changes: 192 additions & 1 deletion Lib/test/test_free_threading/test_type.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import gc
import threading
import unittest

from concurrent.futures import ThreadPoolExecutor
from threading import Thread
from unittest import TestCase

from test.support import threading_helper
from test.support import import_helper, threading_helper

_testinternalcapi = import_helper.import_module("_testinternalcapi")



Expand All @@ -17,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 @@ -162,5 +183,175 @@ def wrapper():
for reader in readers:
reader.join()

def test_setattr_many_subclasses(self):
# gh-155978: Updating a special method updates the slot of every
# affected subclass.
class Base:
pass

subclasses = [type(f"Sub{i}", (Base,), {}) for i in range(100)]

def custom_repr(self):
return "custom repr"

Base.__repr__ = custom_repr
self.assertTrue(all(repr(cls()) == "custom repr"
for cls in subclasses))

del Base.__repr__
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 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.
Loading
Loading