Skip to content

[Bug]: itertools.groupby group iterator re-entered from the key's __eq__ returns a Java null, which crashes with NullPointerException #1276

Open

Description

@taxor03

Describe the bug

When the key's __eq__ advances the same group iterator while groupby is comparing keys (the case from bpo-30347), the outer next() on the group iterator returns a Java null instead of a value or exhaustion. Using that result terminates the process with a Java NullPointerException (exit status 1). CPython returns None here (the default given to next()).

Operating system

macOS

CPU architecture

ARM64

GraalPy version

25.4.4.1.1 (Python 3.13.14), community build from the GitHub release. The code involved is unchanged on master d0565ae.

JDK version

No response

Context configuration

No response

Steps to reproduce

from itertools import groupby

class K:
    def __init__(self, v):
        pass
    def __eq__(self, other):
        global i
        i += 1
        if i == 1:
            next(g, None)  # advance the same group iterator from inside __eq__
        return True

i = 0
g = next(groupby(range(10), K))[1]
print(repr(next(g, None)))
print(repr(next(g, None)))
print("done")

GraalPy prints 0, then exits with the NullPointerException below.

Expected behavior

CPython 3.13.18 (same on 3.12.16):

0
None
done

Stack trace

java.lang.NullPointerException: Null receiver values are not supported by libraries.
	at org.graalvm.truffle/com.oracle.truffle.api.library.LibraryFactory.dispatch(LibraryFactory.java:550)
	at org.graalvm.truffle/com.oracle.truffle.api.library.LibraryFactory.create(LibraryFactory.java:302)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetForeignObjectClassNodeGen.executeAndSpecialize(GetForeignObjectClassNodeGen.java:143)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetForeignObjectClassNodeGen.execute(GetForeignObjectClassNodeGen.java:96)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNode.noRegisteredClass(GetRegisteredClassNode.java:95)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNodeGen.executeAndSpecialize(GetRegisteredClassNodeGen.java:183)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetRegisteredClassNodeGen.execute(GetRegisteredClassNodeGen.java:157)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNode.getForeign(GetClassNode.java:200)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNodeGen$Inlined.executeAndSpecialize(GetClassNodeGen.java:476)
	at org.graalvm.py/com.oracle.graal.python.nodes.object.GetClassNodeGen$Inlined.execute(GetClassNodeGen.java:393)
	at org.graalvm.py/com.oracle.graal.python.builtins.objects.type.TpSlots$GetObjectSlotsNode.doIt(TpSlots.java:2085)
	at org.graalvm.py/com.oracle.graal.python.builtins.objects.type.TpSlotsFactory$GetObjectSlotsNodeGen$Inlined.execute(TpSlotsFactory.java:342)
	at org.graalvm.py/com.oracle.graal.python.lib.PyObjectReprAsObjectNode.repr(PyObjectReprAsObjectNode.java:105)
	at org.graalvm.py/com.oracle.graal.python.lib.PyObjectReprAsObjectNodeGen$Inlined.execute(PyObjectReprAsObjectNodeGen.java:329)
	at org.graalvm.py/com.oracle.graal.python.builtins.modules.BuiltinFunctions$ReprNode.repr(BuiltinFunctions.java:1711)
	at org.graalvm.py/com.oracle.graal.python.builtins.modules.BuiltinFunctionsFactory$ReprNodeFactory$ReprNodeGen.execute(BuiltinFunctionsFactory.java:7030)

Additional context

Assigning the result instead (x = next(g, None)) does not crash, but x is then unbound: the next line that reads it raises NameError: name 'x' is not defined.

test_itertools.RegressionTests.test_issue30347_2 passes because it discards the result of next(g, None).

Root Cause

GrouperBuiltins.NextNode reads gbo.getCurrValue() after the key comparison (GrouperBuiltins.java#L145-L155). The comparison calls the key's __eq__, and the nested next() there takes the current value and sets it to null, so the outer call returns that null. CPython's _grouper_next also reads currvalue after the comparison, but a NULL return without an exception set means exhaustion there (itertoolsmodule.c#L701-L734).

Fix Suggestion

In NextNode, treat a null current value after the comparison as exhaustion (throw iteratorExhausted()), as CPython does.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions