Skip to content

Commit 07ffdaf

Browse files
committed
gh-152166: Fix array.array.fromlist() exposing uninitialized memory on reentrant resize
array.array.fromlist() preallocated n slots with array_resize() and then filled them at an index recomputed from the live Py_SIZE(self) each iteration, guarding only against the source list changing size. When an element's __index__ resized self as a side effect of the setitem call, the write index slid forward, the reserved slots were left unwritten, and the array exposed uninitialized heap memory (with the items misplaced) on a successful return. Fill the fixed slot old_size + i instead, and raise RuntimeError if self is resized mid-iteration, mirroring the existing list-mutation guard. This is distinct from gh-144128/gh-144138, which fixed a use-after-free in the *_setitem conversion helpers and did not touch fromlist's index logic.
1 parent 30aeeb3 commit 07ffdaf

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

Lib/test/test_array.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ def __index__(self):
8282
with self.assertRaises(TypeError):
8383
a.fromlist(lst)
8484

85+
def test_fromlist_reentrant_self_resize(self):
86+
# gh-152166: if an element's __index__ resizes the array being
87+
# filled, fromlist() must not skip the preallocated slots (which
88+
# exposes uninitialized memory) or misplace items. It raises
89+
# RuntimeError and rolls back, mirroring the list-mutation guard.
90+
for label, mutate in (("grow", lambda a: a.append(0)),
91+
("shrink", lambda a: a.pop())):
92+
for typecode in ('i', 'I', 'l', 'L', 'q', 'Q'):
93+
with self.subTest(typecode=typecode, mutate=label):
94+
a = array.array(typecode, [1, 2, 3])
95+
before = a.tolist()
96+
97+
class Evil:
98+
def __index__(self, _a=a, _m=mutate):
99+
_m(_a)
100+
return 0
101+
102+
with self.assertRaises(RuntimeError):
103+
a.fromlist([Evil(), 4, 5])
104+
# The failed call must leave the array unchanged.
105+
self.assertEqual(a.tolist(), before)
106+
85107
def test_typecodes(self):
86108
self.assertIsInstance(array.typecodes, tuple)
87109
for typecode in array.typecodes:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`array.array.fromlist` exposing uninitialized memory (and misplacing
2+
items) when an element's :meth:`~object.__index__` method resizes the array
3+
during the conversion. The reserved slots are now filled at a fixed offset and
4+
the operation raises :exc:`RuntimeError` if the array is resized mid-iteration.

Modules/arraymodule.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,8 +1805,12 @@ array_array_fromlist_impl(arrayobject *self, PyObject *list)
18051805
return NULL;
18061806
for (i = 0; i < n; i++) {
18071807
PyObject *v = PyList_GET_ITEM(list, i);
1808-
if ((*self->ob_descr->setitem)(self,
1809-
Py_SIZE(self) - n + i, v) != 0) {
1808+
/* setitem may run arbitrary Python (e.g. __index__), which can
1809+
resize self. Write to the fixed slot reserved above rather
1810+
than an offset recomputed from the live Py_SIZE, and bail out
1811+
if self was resized -- otherwise we would skip a preallocated
1812+
slot (exposing uninitialized memory) or misplace items. */
1813+
if ((*self->ob_descr->setitem)(self, old_size + i, v) != 0) {
18101814
array_resize(self, old_size);
18111815
return NULL;
18121816
}
@@ -1816,6 +1820,12 @@ array_array_fromlist_impl(arrayobject *self, PyObject *list)
18161820
array_resize(self, old_size);
18171821
return NULL;
18181822
}
1823+
if (Py_SIZE(self) != old_size + n) {
1824+
PyErr_SetString(PyExc_RuntimeError,
1825+
"array changed size during iteration");
1826+
array_resize(self, old_size);
1827+
return NULL;
1828+
}
18191829
}
18201830
}
18211831
Py_RETURN_NONE;

0 commit comments

Comments
 (0)