Skip to content

array: *_setitem functions & co may crash on re-entrant __index__ #142555

Closed

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:

Details
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

Details
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

Details
=================================================================
==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

Linked PRs

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Dec 11, 2025
  2. changed the title [-]Null pointer dereference in `array.__setitem__` via re-entrant `__index__`[/-] [+]`array`: `*_setitem` functions & co may crash on re-entrant `__index__`[/+] on Dec 17, 2025
  3. added a commit that references this issue on Feb 2, 2026
  4. added 2 commits that reference this issue on Feb 2, 2026
  5. added a commit that references this issue on Feb 2, 2026
  6. added a commit that references this issue on Feb 3, 2026
  7. encukou commented on Feb 3, 2026

    @encukou
    Member

    Thank you @jackfromeast for the report, and @aisk for the fix!

  8. added a commit that references this issue on Feb 3, 2026
  9. added a commit that references this issue on Feb 15, 2026
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