Skip to content

Null pointer dereference in II_setitem via re-entrant __index__ during array.extend #142882

Closed as duplicate

Description

@jackfromeast

What happened?

During array.extend, _PyNumber_Index can invoke a user __index__ that clears the array, so II_setitem resumes with ap->ob_item already NULL and the next write through it dereferences null.

Proof of Concept:

from array import array

buf = array("I", range(15))

class ReentrantIndex:
    seen = False

    def __index__(self):
        if ReentrantIndex.seen:
            buf.clear()
            return 2
        ReentrantIndex.seen = True
        return 1

buf.extend([ReentrantIndex()])

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 27 2025, 21:34:13) Exception 1
Python 3.10.19+ (heads/3.10:014261980b1, Oct 27 2025, 21:19:00) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 27 2025, 21:20:35) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 27 2025, 21:27:07) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 27 2025, 21:28:49) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 27 2025, 21:30:55) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 27 2025, 21:32:37) [Clang 18.1.3 (1ubuntu1)] ASAN 1

Vulnerable Code:

static int
II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
{
    unsigned long x;
    int do_decref = 0; /* if nb_int was called */

    if (!PyLong_Check(v)) {
        // Bug: Trigger the __index__ of the value which clears the array's buffer
        v = _PyNumber_Index(v);
        if (NULL == v) {
            return -1;
        }
        do_decref = 1;
    }
    x = PyLong_AsUnsignedLong(v);
    if (x == (unsigned long)-1 && PyErr_Occurred()) {
        if (do_decref) {
            Py_DECREF(v);
        }
        return -1;
    }
    if (x > UINT_MAX) {
        PyErr_SetString(PyExc_OverflowError,
                        "unsigned int is greater than maximum");
        if (do_decref) {
            Py_DECREF(v);
        }
        return -1;
    }
    if (i >= 0)
        // Crash: ap->ob_item has been set to null
        ((unsigned int *)ap->ob_item)[i] = (unsigned int)x;

    if (do_decref) {
        Py_DECREF(v);
    }
    return 0;
}

Sanitizer Output:

=================================================================
==1084586==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000003c (pc 0x742b7c1e6f14 bp 0x7fffe1ed5ec0 sp 0x7fffe1ed5e90 T0)
==1084586==The signal is caused by a WRITE memory access.
==1084586==Hint: address points to the zero page.
    #0 0x742b7c1e6f14 in II_setitem Modules/arraymodule.c:433
    #1 0x742b7c1e8e29 in ins1 Modules/arraymodule.c:713
    #2 0x742b7c1eb22f in array_iter_extend Modules/arraymodule.c:1061
    #3 0x742b7c1eb3d5 in array_do_extend Modules/arraymodule.c:1080
    #4 0x742b7c1eb67f in array_array_extend_impl Modules/arraymodule.c:1346
    #5 0x742b7c1ed191 in array_array_extend Modules/clinic/arraymodule.c.h:242
    #6 0x56323d0fd3c9 in method_vectorcall_FASTCALL_KEYWORDS_METHOD Objects/descrobject.c:381
    #7 0x56323d0dde7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #8 0x56323d0ddf72 in PyObject_Vectorcall Objects/call.c:327
    #9 0x56323d35c056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #10 0x56323d39fe54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #11 0x56323d3a0148 in _PyEval_Vector Python/ceval.c:2001
    #12 0x56323d3a03f8 in PyEval_EvalCode Python/ceval.c:884
    #13 0x56323d497507 in run_eval_code_obj Python/pythonrun.c:1365
    #14 0x56323d497723 in run_mod Python/pythonrun.c:1459
    #15 0x56323d49857a in pyrun_file Python/pythonrun.c:1293
    #16 0x56323d49b220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #17 0x56323d49b4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #18 0x56323d4ec74d in pymain_run_file_obj Modules/main.c:410
    #19 0x56323d4ec9b4 in pymain_run_file Modules/main.c:429
    #20 0x56323d4ee1b2 in pymain_run_python Modules/main.c:691
    #21 0x56323d4ee842 in Py_RunMain Modules/main.c:772
    #22 0x56323d4eea2e in pymain_main Modules/main.c:802
    #23 0x56323d4eedb3 in Py_BytesMain Modules/main.c:826
    #24 0x56323cf72645 in main Programs/python.c:15
    #25 0x742b7ca2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #26 0x742b7ca2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #27 0x56323cf72574 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: 202d5dbb945f6d5f5a66ad50e2688d56affd6ecb)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Modules/arraymodule.c:433 in II_setitem
==1084586==ABORTING

Activity

  1. aisk commented on Dec 17, 2025

    @aisk
    Member

    Although this is not the same method, I think this null pointer dereference shares the same pattern as #142555.

    In both cases, they accept a user defined argument that implements the __index__ method, and we should check whether it changes the array in *_setitem.

    I'm working on a PR #142713 that addresses all of these patterns.

  2. jackfromeast commented on Dec 17, 2025

    @jackfromeast
    Author

    Thank you, @aisk! Sounds great to me!

  3. picnixz commented on Dec 17, 2025

    @picnixz
    Member

    I'm going to consider this as a duplicate of #142555. All *_set* functions suffer from this I think (and maybe more), but let's only have one issue

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

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

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions