gh-156310: Make the iter() sequence fallback iterator safe in free-threaded build by akx · Pull Request #156311 · python/cpython · GitHub
Skip to content
Merged
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
58 changes: 58 additions & 0 deletions Lib/test/test_free_threading/test_iteration.py
66 changes: 66 additions & 0 deletions Lib/test/test_iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
import unittest
from test import support
from test.support import cpython_only
from test.support.os_helper import TESTFN, unlink
from test.support import check_free_after_iterating, ALWAYS_EQ, NEVER_EQ
Expand Down Expand Up @@ -249,6 +250,71 @@ def test_mutating_seq_class_exhausted_iter(self):
self.assertEqual(list(empit), [5, 6])
self.assertEqual(list(a), [0, 1, 2, 3, 4, 5, 6])

@support.refcount_test
def test_seq_class_reentrant_exhaustion(self):
# gh-156310: a re-entrant next() from inside __getitem__ (or from
# __del__ of the IndexError instance) that exhausts the iterator
# used to make the outer next() DECREF the sequence a second time.
it = None

class ReentrantGetItem:
def __init__(self):
self.calls = 0

def __getitem__(self, i):
self.calls += 1
if self.calls == 1:
for _ in it:
pass
raise IndexError(i)

seq = ReentrantGetItem()
refcount = sys.getrefcount(seq)
it = iter(seq)
self.assertEqual(list(it), [])
del it
support.gc_collect()
self.assertEqual(sys.getrefcount(seq), refcount)

class ReentrantIndexError(IndexError):
def __del__(self):
try:
next(it)
except StopIteration:
pass

class RaiseReentrant:
def __getitem__(self, i):
raise ReentrantIndexError(i)

seq = RaiseReentrant()
refcount = sys.getrefcount(seq)
it = iter(seq)
self.assertEqual(list(it), [])
del it
support.gc_collect()
self.assertEqual(sys.getrefcount(seq), refcount)

# An outer __getitem__ that succeeds after a re-entrant next()
# exhausted the iterator must not revive it.
class ReviveGetItem:
def __init__(self):
self.calls = 0

def __getitem__(self, i):
self.calls += 1
if self.calls == 1:
for _ in it:
pass
if i >= 3:
raise IndexError(i)
return i

it = iter(ReviveGetItem())
self.assertEqual(next(it), 0)
self.assertEqual(list(it), [])
self.assertEqual(it.__length_hint__(), 0)

def test_reduce_mutating_builtins_iter(self):
# This is a reproducer of issue #101765
# where iter `__reduce__` calls could lead to a segfault or SystemError
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fix memory safety issues in the :func:`iter` fallback for objects that
implement :meth:`~object.__getitem__` without :meth:`~object.__iter__`
(``PySeqIter_Type``). Sharing an iterator between threads in the
free-threaded build could use the underlying sequence after it was freed,
and re-entrant exhaustion in the default build could decrement the sequence's
reference count twice. Concurrent iteration may still see duplicate or
missing items, but it no longer corrupts the interpreter state.
49 changes: 34 additions & 15 deletions Objects/iterobject.c
Loading