@@ -645,6 +645,44 @@ def test_join(self):
645645 with self .assertRaises (TypeError ):
646646 dot_join ([memoryview (b"ab" ), "cd" , b"ef" ])
647647
648+ def test_join_reentrant_buffer_mutation (self ):
649+ # An item's __buffer__() may run Python that drops the last reference
650+ # to that item by mutating the joined sequence.
651+ # See: https://github.com/python/cpython/issues/151295
652+ def make_seq (mutate ):
653+ # The mutating item is only referenced from the list slot, so
654+ # mutate() drops its last reference mid-join.
655+ class Item :
656+ def __buffer__ (self , flags ):
657+ mutate (seq )
658+ return memoryview (b'x' )
659+ seq = [b'a' , Item (), b'c' ]
660+ return seq
661+
662+ class Benign :
663+ def __buffer__ (self , flags ):
664+ return memoryview (b'x' )
665+
666+ for sep in (self .type2test (b'' ), self .type2test (b'::' )):
667+ with self .subTest (sep = sep ):
668+ # Clearing the list changes its length, which is reported as a
669+ # RuntimeError.
670+ seq = make_seq (lambda seq : seq .clear ())
671+ self .assertRaises (RuntimeError , sep .join , seq )
672+
673+ # Replacing the item in place keeps the list length unchanged,
674+ # so the size-change recheck cannot fire; only keeping the item
675+ # alive across __buffer__() prevents the use-after-free, and
676+ # the join uses the buffer returned by __buffer__().
677+ def replace (seq ):
678+ seq [1 ] = b'z'
679+ seq = make_seq (replace )
680+ self .assertEqual (sep .join (seq ), sep .join ([b'a' , b'x' , b'c' ]))
681+
682+ # A benign __buffer__() that does not mutate joins normally.
683+ self .assertEqual (sep .join ([Benign (), b'Y' , Benign ()]),
684+ sep .join ([b'x' , b'Y' , b'x' ]))
685+
648686 def test_count (self ):
649687 b = self .type2test (b'mississippi' )
650688 i = 105
0 commit comments