Skip to content

itertools.zip_longest use-after-free via re-entrant iterator exhaust #154672

Description

@tonghuaroot

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions