-
-
Notifications
You must be signed in to change notification settings - Fork 33.6k
Open
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
Description
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==ABORTINGMetadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump