Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Lib/test/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ def __index__(self):
with self.assertRaises(TypeError):
a.fromlist(lst)

def test_fromlist_reentrant_self_resize(self):
# gh-152166: if an element's __index__ resizes the array being
# filled, fromlist() must not skip the preallocated slots (which
# exposes uninitialized memory) or misplace items. It raises
# RuntimeError and rolls back, mirroring the list-mutation guard.
for label, mutate in (("grow", lambda a: a.append(0)),
("shrink", lambda a: a.pop())):
for typecode in ('i', 'I', 'l', 'L', 'q', 'Q'):
with self.subTest(typecode=typecode, mutate=label):
a = array.array(typecode, [1, 2, 3])
before = a.tolist()

class Evil:
def __index__(self, _a=a, _m=mutate):
_m(_a)
return 0

with self.assertRaises(RuntimeError):
a.fromlist([Evil(), 4, 5])
# The failed call must leave the array unchanged.
self.assertEqual(a.tolist(), before)

def test_typecodes(self):
self.assertIsInstance(array.typecodes, tuple)
for typecode in array.typecodes:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :meth:`array.array.fromlist` exposing uninitialized memory (and misplacing
items) when an element's :meth:`~object.__index__` method resizes the array
during the conversion. The reserved slots are now filled at a fixed offset and
the operation raises :exc:`RuntimeError` if the array is resized mid-iteration.
14 changes: 12 additions & 2 deletions Modules/arraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1805,8 +1805,12 @@ array_array_fromlist_impl(arrayobject *self, PyObject *list)
return NULL;
for (i = 0; i < n; i++) {
PyObject *v = PyList_GET_ITEM(list, i);
if ((*self->ob_descr->setitem)(self,
Py_SIZE(self) - n + i, v) != 0) {
/* setitem may run arbitrary Python (e.g. __index__), which can
resize self. Write to the fixed slot reserved above rather
than an offset recomputed from the live Py_SIZE, and bail out
if self was resized -- otherwise we would skip a preallocated
slot (exposing uninitialized memory) or misplace items. */
if ((*self->ob_descr->setitem)(self, old_size + i, v) != 0) {
array_resize(self, old_size);
return NULL;
}
Expand All @@ -1816,6 +1820,12 @@ array_array_fromlist_impl(arrayobject *self, PyObject *list)
array_resize(self, old_size);
return NULL;
}
if (Py_SIZE(self) != old_size + n) {
PyErr_SetString(PyExc_RuntimeError,
"array changed size during iteration");
array_resize(self, old_size);
return NULL;
}
}
}
Py_RETURN_NONE;
Expand Down
Loading