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
Detailsint 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
- gh-142555: Fix null pointer dereference in array.__setitem__ via re-entrant __index__ #142713
- [3.14] gh-142555: Fix null pointer dereference in array.__setitem__ via re-entrant __index__ (GH-142713) #144396
- [3.13] gh-142555: Fix null pointer dereference in array.__setitem__ via re-entrant __index__ (GH-142713) #144397