Bug description
itertools.zip_longest has a use-after-free when an iterator's __next__ re-enters the zip_longest and exhausts a sibling iterator.
zip_longest_next_lock_held() borrows iterators from lz->ittuple via PyTuple_GET_ITEM without Py_INCREF, then calls PyIter_Next(it). If the callback re-enters and exhausts the same iterator, the inner call does PyTuple_SET_ITEM(ittuple, i, NULL) + Py_DECREF(it), freeing it while the outer call is still using it.
Crashes with PYTHONMALLOC=debug (SIGSEGV).
from itertools import zip_longest
zl = None
class ReentrantIter:
def __init__(self, it):
self.it = it
def __iter__(self):
return self
def __next__(self):
if zl is not None:
for _ in zl:
pass
return next(self.it)
zl = zip_longest(ReentrantIter(iter([1])), iter([2, 3]))
list(zl) # SIGSEGV
CPython versions tested on
main
Operating systems tested on
macOS
Linked PRs
Bug description
itertools.zip_longesthas a use-after-free when an iterator's__next__re-enters the zip_longest and exhausts a sibling iterator.zip_longest_next_lock_held()borrows iterators fromlz->ittupleviaPyTuple_GET_ITEMwithoutPy_INCREF, then callsPyIter_Next(it). If the callback re-enters and exhausts the same iterator, the inner call doesPyTuple_SET_ITEM(ittuple, i, NULL)+Py_DECREF(it), freeing it while the outer call is still using it.Crashes with
PYTHONMALLOC=debug(SIGSEGV).CPython versions tested on
main
Operating systems tested on
macOS
Linked PRs