Skip to content
Closed
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
31 changes: 31 additions & 0 deletions Lib/test/test_asyncgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,37 @@ def test_aiter_callable_exhausted(self):
with self.assertRaises(StopAsyncIteration):
self.loop.run_until_complete(anext(it))

def test_aiter_callable_sentinel_reentrant_exhaustion(self):
# gh-158033: a sentinel __eq__ that exhausts the iterator
# re-entrantly must not leave the comparison using a freed
# sentinel.
state = {'stop': False}

async def produce():
return Result()

def spam():
if state['stop']:
raise StopAsyncIteration
return produce()

class Sentinel:
def __eq__(self, other):
state['stop'] = True
try:
ait.__anext__().__await__().send(None)
except StopAsyncIteration:
pass
return NotImplemented

class Result:
def __eq__(self, other):
return NotImplemented

ait = aiter(spam, Sentinel())
with self.assertRaises(StopIteration):
ait.__anext__().__await__().send(None)

def test_aiter_callable_lazy(self):
# The callable is only called when the awaitable is awaited
calls = []
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,24 @@ def test_calliter_setstate(self):
it.__setstate__(((10,), StopIteration))
self.assertEqual(list(it), list(range(10)))

def test_calliter_sentinel_reentrant_setstate(self):
# gh-158031: a sentinel __eq__ that mutates the iterator
# re-entrantly must not leave the comparison using a freed
# sentinel.
class Sentinel:
def __eq__(self, other):
it.__setstate__(((), StopIteration))
return NotImplemented

class Result:
def __eq__(self, other):
return NotImplemented

it = iter(lambda: Result(), Sentinel())
self.assertIsInstance(next(it), Result)
# __setstate__ cleared the sentinel; iteration still works.
self.assertIsInstance(next(it), Result)

def test_iter_function_concealing_reentrant_exhaustion(self):
# gh-101892: Test two-argument iter() with a function that
# exhausts its associated iterator but forgets to either return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix use-after-free in :func:`iter` and :func:`aiter` callables with a
sentinel: the sentinel comparison could run code that replaced or released
the sentinel (for example via ``__setstate__`` or a re-entrant
``__anext__``), leaving the comparison using freed memory. A strong
reference is now held for the duration of the comparison.
14 changes: 12 additions & 2 deletions Objects/iterobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,12 @@ calliter_iternext(PyObject *op)
if (it->it_sentinel == NULL) {
return result; /* Common case, fast path */
}
int ok = PyObject_RichCompareBool(it->it_sentinel, result, Py_EQ);
/* The comparison can run code that mutates the iterator
(e.g. __setstate__), so hold a strong reference to the
sentinel while it is in use. */
PyObject *sentinel = Py_NewRef(it->it_sentinel);
int ok = PyObject_RichCompareBool(sentinel, result, Py_EQ);
Py_DECREF(sentinel);
if (ok == 0) {
return result; /* Common case, fast path */
}
Expand Down Expand Up @@ -641,7 +646,12 @@ acallawaitable_handle_error(acallawaitableobject *aw)
}
int ok = 0;
if (it->it_sentinel != NULL) {
ok = PyObject_RichCompareBool(it->it_sentinel, value, Py_EQ);
/* The comparison can run code that exhausts the iterator
re-entrantly, so hold a strong reference to the sentinel
while it is in use. */
PyObject *sentinel = Py_NewRef(it->it_sentinel);
ok = PyObject_RichCompareBool(sentinel, value, Py_EQ);
Py_DECREF(sentinel);
}
if (ok == 0) {
(void)_PyGen_SetStopIterationValue(value);
Expand Down
Loading