Skip to content

itertools.zip_longest stores NULL in GC-tracked ittuple when iterators exhaust #148181

@devdanzin

Description

@devdanzin

Crash report

What happened?

itertools.zip_longest stores NULL in its internal ittuple (the tuple of iterators) when an iterator is exhausted, at Modules/itertoolsmodule.c:3847,3877:

PyTuple_SET_ITEM(lz->ittuple, i, NULL);
Py_DECREF(it);

The ittuple is GC-tracked and visited by zip_longest_traverse. When exposed via gc.get_referents(), the tuple contains NULL items. On builds without the PyUnicodeWriter_WriteRepr NULL fix (gh-146056), repr() of this tuple segfaults. On builds with the fix, it displays <NULL>.

This is the same class of bug as gh-146056 (NULL items in GC-tracked containers), but in itertools.zip_longest rather than xml.etree.ElementTree.

Reproducer

import itertools
import gc

zl = itertools.zip_longest(iter([1]), iter([1, 2, 3]))
next(zl)  # (1, 1) — both iterators alive
next(zl)  # (None, 2) — first iterator exhausted, ittuple[0] set to NULL

# Expose the internal ittuple via gc.get_referents
refs = gc.get_referents(zl)
for r in refs:
    if isinstance(r, tuple):
        print(repr(r))  # Segfault on builds without gh-146056 fix
                         # Shows (<NULL>, <list_iterator ...>) on builds with it

Suggested fix

Replace NULL with Py_None as the sentinel for exhausted iterators. Py_None can never be a valid iterator in the ittuple (passing None as an iterable to zip_longest fails at PyObject_GetIter in zip_longest_new).

--- a/Modules/itertoolsmodule.c
+++ b/Modules/itertoolsmodule.c
@@ -3832,7 +3832,7 @@
         for (i=0 ; i < tuplesize ; i++) {
             it = PyTuple_GET_ITEM(lz->ittuple, i);
-            if (it == NULL) {
+            if (it == Py_None) {
                 item = Py_NewRef(lz->fillvalue);
             } else {
@@ -3844,7 +3844,7 @@
                     } else {
                         item = Py_NewRef(lz->fillvalue);
-                        PyTuple_SET_ITEM(lz->ittuple, i, NULL);
+                        PyTuple_SET_ITEM(lz->ittuple, i, Py_NewRef(Py_None));
                         Py_DECREF(it);
                     }

The same change applies to both the result-reuse path (line ~3835/3847) and the new-result path (line ~3865/3877) — 4 lines total.

Found with assistance from Claude Code, while investigating a related crash reported by @zhuyifei1999.

CPython versions tested on:

CPython main branch, 3.15, 3.14

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a7+ free-threading build (heads/main:4d0e8ee649c, Mar 29 2026, 23:20:29) [Clang 21.1.2 (2ubuntu6)]

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