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;
}
What happened?
A user-defined
__index__can clear/shrink the targetarrayduring index conversion. The bounds check happens before the callback, but the write inb_setitemhappens after, using a stale buffer and causing a write into freed/zero-length memory.Proof of Concept:
Affected Versions:
Details
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30)Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0]Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0]Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0]Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0]Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0]Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0]Vulnerable Code Snippet
Details
Sanitizer
Details
Linked PRs