@@ -929,50 +929,92 @@ def __eq__(self, other):
929929
930930        fut .remove_done_callback (evil ())
931931
932+     # Sanity checks for callback tuples corruption and Use-After-Free. 
933+     # Special thanks to Nico-Posada for the original PoCs and ideas. 
934+     # See https://github.com/python/cpython/issues/125789. 
935+ 
932936    def  test_schedule_callbacks_list_mutation_3 (self ):
933-         raise  NotImplemented 
937+         raise  NotImplementedError 
934938
935939    def  _test_schedule_callbacks_list_mutation_3 (self , exc_type , exc_text = None ):
936-         # see https://github.com/python/cpython/issues/125789 for details 
937-         fut  =  self ._new_future ()
938- 
939940        class  evil :
940941            def  __eq__ (self , other ):
941942                global  mem 
942943                mem  =  other 
943944                return  False 
944945
945946        cb_pad  =  lambda : ...
947+ 
948+         fut  =  self ._new_future ()
946949        fut .add_done_callback (cb_pad )  # sets fut->fut_callback0 
947-         fut .add_done_callback (evil ())  # sets first item in fut->fut_callbacks 
948-         # Consume fut->fut_callback0 callback but checks the remaining callbacks, 
949-         # thereby invoking evil.__eq__(). 
950-         fut .remove_done_callback (cb_pad )
950+         fut .add_done_callback (evil ())  # sets fut->fut_callbacks[0] 
951+         # Consume fut->fut_callback0 callback and set it to NULL before 
952+         # checking fut->fut_callbacks, thereby invoking evil.__eq__(). 
953+         removed  =  fut .remove_done_callback (cb_pad )
954+         self .assertEqual (removed , 1 )
951955        self .assertIs (mem , cb_pad )
952- 
953-         fake  =  (
954-             (0 ).to_bytes (8 , 'little' ) + 
955-             id (bytearray ).to_bytes (8 , 'little' ) + 
956-             (2  **  63  -  1 ).to_bytes (8 , 'little' ) + 
957-             (0 ).to_bytes (24 , 'little' )
958-         )
959- 
960-         i2f  =  lambda  num : 5e-324  *  num 
961-         fut ._callbacks [0 ] =  complex (0 , i2f (id (fake ) +  bytes .__basicsize__  -  1 ))
962- 
963-         # We want to call once again evil.__eq__() to set 'mem' to our 
964-         # malicious bytearray. However, since we manually modified the 
965-         # callbacks list, we will not be able to by-pass the checks. 
956+         # Set the malicious callback tuple and trigger a type check on it 
957+         # by re-invoking evil.__eq__() through remove_done_callback(). 
958+         fut ._callbacks [0 ] =  complex (0 , 0 )
966959        if  exc_text  is  None :
967960            self .assertRaises (exc_type , fut .remove_done_callback , evil ())
968961        else :
969-             self .assertRaisesRegex (exc_type , exc_text ,  fut . remove_done_callback ,  evil ()) 
970- 
962+             self .assertRaisesRegex (exc_type , exc_text ,
963+                                     fut . remove_done_callback ,  evil ()) 
971964        self .assertIs (mem , cb_pad )
972965
973-     # Use-After-Free sanity checks. 
974-     # Credits to Nico-Posada for the PoCs. 
975-     # See https://github.com/python/cpython/pull/125833. 
966+     def  _evil_call_soon_event_loop (self , evil_call_soon ):
967+         fake_event_loop  =  lambda : ...
968+         fake_event_loop .call_soon  =  evil_call_soon 
969+         fake_event_loop .get_debug  =  lambda : False   # suppress traceback 
970+         return  fake_event_loop 
971+ 
972+     def  test_evil_call_soon_list_mutation_1 (self ):
973+         def  evil_call_soon (* args , ** kwargs ):
974+             fut ._callbacks .clear ()
975+ 
976+         loop  =  self ._evil_call_soon_event_loop (evil_call_soon )
977+         with  mock .patch .object (self , 'loop' , loop ):
978+             fut  =  self ._new_future ()
979+             self .assertIs (fut .get_loop (), loop )
980+ 
981+             cb_pad  =  lambda : ...
982+             fut .add_done_callback (cb_pad )
983+             fut .add_done_callback (None )
984+             fut .add_done_callback (None )
985+ 
986+             removed  =  fut .remove_done_callback (cb_pad )
987+             self .assertEqual (removed , 1 )
988+             self .assertEqual (len (fut ._callbacks ), 2 )
989+             fut .set_result ("boom" )
990+             # When there are no more callbacks, the Python implementation 
991+             # returns an empty list but the C implementation returns None. 
992+             self .assertIn (fut ._callbacks , (None , []))
993+ 
994+     def  test_evil_call_soon_list_mutation_2 (self ):
995+         raise  NotImplementedError 
996+ 
997+     def  _test_evil_call_soon_list_mutation_2 (self , exc_type , exc_text = None ):
998+         def  evil_call_soon (* args , ** kwargs ):
999+             fut ._callbacks [1 ] =  complex (0 , 0 )
1000+ 
1001+         loop  =  self ._evil_call_soon_event_loop (evil_call_soon )
1002+         with  mock .patch .object (self , 'loop' , loop ):
1003+             fut  =  self ._new_future ()
1004+             self .assertIs (fut .get_loop (), loop )
1005+ 
1006+             cb_pad  =  lambda : ...
1007+             fut .add_done_callback (cb_pad )
1008+             fut .add_done_callback (None )
1009+             fut .add_done_callback (None )
1010+             removed  =  fut .remove_done_callback (cb_pad )
1011+             self .assertEqual (removed , 1 )
1012+             self .assertEqual (len (fut ._callbacks ), 2 )
1013+             # The evil 'call_soon' is executed by calling set_result(). 
1014+             if  exc_text  is  None :
1015+                 self .assertRaises (exc_type , fut .set_result , "boom" )
1016+             else :
1017+                 self .assertRaisesRegex (exc_type , exc_text , fut .set_result , "boom" )
9761018
9771019    def  test_use_after_free_fixed_1 (self ):
9781020        fut  =  self ._new_future ()
@@ -990,11 +1032,17 @@ def __eq__(self, value):
9901032        cb_pad  =  lambda : ...
9911033        fut .add_done_callback (cb_pad )  # sets fut->fut_callback0 
9921034        fut .add_done_callback (setup ())  # sets fut->fut_callbacks[0] 
993-         # removes callback from fut->fut_callback0 setting it to NULL 
994-         fut .remove_done_callback (cb_pad )
995-         fut .remove_done_callback (evil ())
1035+         removed  =  fut .remove_done_callback (cb_pad )
1036+         self .assertEqual (removed , 1 )
1037+ 
1038+         # This triggers evil.__eq__(), thereby clearing fut->fut_callbacks 
1039+         # but we will still hold a reference to fut->fut_callbacks[0] until 
1040+         # it is no more needed. 
1041+         removed  =  fut .remove_done_callback (evil ())
1042+         self .assertEqual (removed , 0 )
9961043
9971044    def  test_use_after_free_fixed_2 (self ):
1045+         asserter  =  self 
9981046        fut  =  self ._new_future ()
9991047
10001048        class  cb_pad :
@@ -1003,11 +1051,13 @@ def __eq__(self, other):
10031051
10041052        class  evil (cb_pad ):
10051053            def  __eq__ (self , other ):
1006-                 fut .remove_done_callback (None )
1054+                 removed  =  fut .remove_done_callback (None )
1055+                 asserter .assertEqual (removed , 1 )
10071056                return  NotImplemented 
10081057
10091058        fut .add_done_callback (cb_pad ())
1010-         fut .remove_done_callback (evil ())
1059+         removed  =  fut .remove_done_callback (evil ())
1060+         self .assertEqual (removed , 1 )
10111061
10121062
10131063@unittest .skipUnless (hasattr (futures , '_CFuture' ), 
@@ -1019,7 +1069,12 @@ def _new_future(self):
10191069        return  futures ._CFuture (loop = self .loop )
10201070
10211071    def  test_schedule_callbacks_list_mutation_3 (self ):
1022-         super ()._test_schedule_callbacks_list_mutation_3 (RuntimeError , 'corrupted' )
1072+         errmsg  =  'corrupted callback tuple' 
1073+         super ()._test_schedule_callbacks_list_mutation_3 (RuntimeError , errmsg )
1074+ 
1075+     def  test_evil_call_soon_list_mutation_2 (self ):
1076+         errmsg  =  'corrupted callback tuple' 
1077+         super ()._test_evil_call_soon_list_mutation_2 (RuntimeError , errmsg )
10231078
10241079
10251080@unittest .skipUnless (hasattr (futures , '_CFuture' ), 
@@ -1033,7 +1088,12 @@ class CSubFuture(futures._CFuture):
10331088        return  CSubFuture (loop = self .loop )
10341089
10351090    def  test_schedule_callbacks_list_mutation_3 (self ):
1036-         super ()._test_schedule_callbacks_list_mutation_3 (RuntimeError , 'corrupted' )
1091+         errmsg  =  'corrupted callback tuple' 
1092+         super ()._test_schedule_callbacks_list_mutation_3 (RuntimeError , errmsg )
1093+ 
1094+     def  test_evil_call_soon_list_mutation_2 (self ):
1095+         errmsg  =  'corrupted callback tuple' 
1096+         super ()._test_evil_call_soon_list_mutation_2 (RuntimeError , errmsg )
10371097
10381098
10391099class  PyFutureDoneCallbackTests (BaseFutureDoneCallbackTests ,
@@ -1045,6 +1105,13 @@ def _new_future(self):
10451105    def  test_schedule_callbacks_list_mutation_3 (self ):
10461106        super ()._test_schedule_callbacks_list_mutation_3 (TypeError )
10471107
1108+     def  test_evil_call_soon_list_mutation_2 (self ):
1109+         # For this test, the Python implementation raises an IndexError 
1110+         # because the attribute fut._callbacks is set to an empty list 
1111+         # *before* invoking the callbacks, while the C implementation 
1112+         # does not make a temporary copy of the list of callbacks. 
1113+         super ()._test_evil_call_soon_list_mutation_2 (IndexError )
1114+ 
10481115
10491116class  BaseFutureInheritanceTests :
10501117
0 commit comments