1+ import gc
12import threading
23import unittest
34
1920class A :
2021 attr = 1
2122
23+ def world_stoppers ():
24+ """Finite workloads that contend for the stop-the-world mutex.
25+
26+ Waiting for the mutex releases the type lock, so that other threads
27+ can change types in the meantime. Use a short pause rather than full collections
28+ to prolong the contention without depending on the size of the heap.
29+ """
30+ def stopper ():
31+ for _ in range (100 ):
32+ _testinternalcapi .stop_the_world ()
33+
34+ def pauser ():
35+ for _ in range (100 ):
36+ _testinternalcapi .stop_the_world (0.0001 )
37+
38+ return [stopper , pauser ]
39+
40+
2241@threading_helper .requires_working_threading ()
2342class TestType (TestCase ):
2443 def test_attr_cache (self ):
@@ -325,9 +344,8 @@ def wrapper():
325344 reader .join ()
326345
327346 def test_setattr_many_subclasses (self ):
328- # gh-155978: Updating a special method queues a slot update for every
329- # affected subclass. Keep enough subclasses alive to require
330- # heap-allocated queue chunks in addition to the stack chunk.
347+ # gh-155978: Updating a special method updates the slot of every
348+ # affected subclass.
331349 class Base :
332350 pass
333351
@@ -344,6 +362,11 @@ def custom_repr(self):
344362 self .assertTrue (all (repr (cls ()) != "custom repr"
345363 for cls in subclasses ))
346364
365+ # The slot update must not keep references to the types.
366+ del subclasses
367+ gc .collect ()
368+ self .assertEqual (Base .__subclasses__ (), [])
369+
347370 def test_concurrent_setattr_deadlock (self ):
348371 # gh-155400: two threads assigning to a special method of the same
349372 # class could deadlock. One thread held the type lock and waited for
@@ -401,6 +424,151 @@ class B(A):
401424 with threading_helper .start_threads (threads ):
402425 pass
403426
427+ def check_stop_the_world_under_type_lock (self , setter ):
428+ # gh-158195: Python code that runs with the type lock held and stops
429+ # the world could deadlock with a thread that held the type lock
430+ # pinned while it waited for the stop-the-world mutex (__bases__
431+ # assignment). The first thread released the type lock while it was
432+ # blocked on the stop-the-world mutex, was handed the mutex, and then
433+ # waited for the type lock while owning the mutex.
434+ def basesetter ():
435+ class A :
436+ pass
437+ class B :
438+ pass
439+ class C (A ):
440+ pass
441+ for _ in range (100 ):
442+ C .__bases__ = (B ,)
443+ C .__bases__ = (A ,)
444+
445+ def stopper ():
446+ for _ in range (100 ):
447+ _testinternalcapi .stop_the_world ()
448+
449+ # The stopper keeps the stop-the-world mutex contended, which makes
450+ # the setters block on it. All workers have finite workloads so
451+ # that repeated invalidation cannot indefinitely delay a setter.
452+ threading_helper .run_concurrently (
453+ [setter , setter , basesetter , stopper ])
454+
455+ def test_stop_the_world_in_mro_deadlock (self ):
456+ # type_ready() calls a metaclass mro() with the type lock held.
457+ N = 50
458+
459+ class Meta (type ):
460+ def mro (cls ):
461+ _testinternalcapi .stop_the_world ()
462+ return type .mro (cls )
463+
464+ def setter ():
465+ for _ in range (N ):
466+ Meta ("K" , (), {})
467+
468+ self .check_stop_the_world_under_type_lock (setter )
469+
470+ def test_reentrant_mro_always_publishes (self ):
471+ # A custom mro() that updates a slot on every call must not make
472+ # the __bases__ assignment retry forever.
473+ class Other :
474+ pass
475+
476+ class Meta (type ):
477+ def mro (cls ):
478+ Other .__repr__ = lambda self : "x"
479+ return type .mro (cls )
480+
481+ class A :
482+ pass
483+
484+ class B :
485+ pass
486+
487+ class C (A , metaclass = Meta ):
488+ pass
489+
490+ C .__bases__ = (B ,)
491+ self .assertEqual (C .__mro__ , (C , B , object ))
492+
493+ def test_slot_update_skips_non_str_keys (self ):
494+ # Slots are computed (with the world stopped, for an update) without
495+ # comparing the slot name with non-str keys of the type dicts, which
496+ # could run user code. Here, that code would also update a slot
497+ # every time.
498+ calls = 0
499+
500+ class Other :
501+ pass
502+
503+ class Key :
504+ def __hash__ (self ):
505+ return hash ("__hash__" )
506+
507+ def __eq__ (self , other ):
508+ nonlocal calls
509+ calls += 1
510+ Other .__repr__ = lambda self : "x"
511+ return False
512+
513+ class Base :
514+ pass
515+
516+ with self .assertWarns (RuntimeWarning ):
517+ Sub = type ("Sub" , (Base ,), {Key (): None })
518+ self .assertEqual (calls , 0 )
519+
520+ Base .__hash__ = None
521+ self .assertEqual (calls , 0 )
522+ with self .assertRaises (TypeError ):
523+ hash (Sub ())
524+ del Base .__hash__
525+ hash (Sub ())
526+
527+ def test_concurrent_bases_assignment (self ):
528+ # Several threads reassign __bases__ of the same class, and of its
529+ # base, while others stop the world. The type lock is released while
530+ # waiting for the stop-the-world. At the end, the MRO must agree
531+ # with __bases__.
532+ N = 50
533+
534+ class A :
535+ def meth (self ):
536+ return "A"
537+ def __repr__ (self ):
538+ return "A"
539+ class B :
540+ def meth (self ):
541+ return "B"
542+ def __repr__ (self ):
543+ return "B"
544+ class C (A ):
545+ pass
546+ class D (C ):
547+ pass
548+
549+ def setter ():
550+ for _ in range (N ):
551+ C .__bases__ = (B ,)
552+ C .__bases__ = (A ,)
553+
554+ def subsetter ():
555+ for _ in range (N ):
556+ D .__bases__ = (C , B )
557+ D .__bases__ = (C ,)
558+
559+ threading_helper .run_concurrently (
560+ [setter , setter , subsetter ] + world_stoppers ())
561+
562+ self .assertEqual (C .__bases__ , (A ,))
563+ self .assertEqual (C .__mro__ , (C , A , object ))
564+ self .assertEqual (D .__bases__ , (C ,))
565+ self .assertEqual (D .__mro__ , (D , C , A , object ))
566+ self .assertEqual (D ().meth (), "A" )
567+ self .assertEqual (repr (C ()), "A" )
568+ self .assertEqual (repr (D ()), "A" )
569+ self .assertIn (C , A .__subclasses__ ())
570+ self .assertNotIn (C , B .__subclasses__ ())
571+
404572 def test_class_creation_with_concurrent_setattr (self ):
405573 # Creating a class resolves its slots from the dicts along the MRO
406574 # while holding the type lock, before the class is reachable from
0 commit comments