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
58 changes: 58 additions & 0 deletions Lib/test/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,64 @@ def test_gh_128961(self):
it.__setstate__(0)
self.assertRaises(StopIteration, next, it)

def test_array_validity_after_call_user_method(self):
# gh-142555: Test for null pointer dereference in array.__setitem__
# via re-entrant __index__ or __float__.

def test_clear_array(victim):
class EvilIndex:
def __index__(self):
# Re-entrant mutation: clear the array while __setitem__
# still holds a pointer to the pre-clear buffer.
victim.clear()
return 0

with self.assertRaises(IndexError):
victim[1] = EvilIndex()

self.assertEqual(len(victim), 0)

def test_shrink_array(victim):
class ShrinkIndex:
def __index__(self):
# Re-entrant mutation: change the array size while
# __setitem__ still keep the original size.
victim.pop()
victim.pop()
return 0

with self.assertRaises(IndexError):
victim[1] = ShrinkIndex()

test_clear_array(array.array('b', [0] * 64))
test_shrink_array(array.array('b', [1, 2, 3]))
test_clear_array(array.array('B', [1, 2, 3]))
test_clear_array(array.array('h', [1, 2, 3]))
test_clear_array(array.array('H', [1, 2, 3]))
test_clear_array(array.array('i', [1, 2, 3]))
test_clear_array(array.array('l', [1, 2, 3]))
test_clear_array(array.array('q', [1, 2, 3]))
test_clear_array(array.array('I', [1, 2, 3]))
test_clear_array(array.array('L', [1, 2, 3]))
test_clear_array(array.array('Q', [1, 2, 3]))

def test_clear_array_float(victim):
"""Test array clearing scenario using __float__ method"""
class EvilFloat:
def __float__(self):
# Re-entrant mutation: clear the array while __setitem__
# still holds a pointer to the pre-clear buffer.
victim.clear()
return 0.0

with self.assertRaises(IndexError):
victim[1] = EvilFloat()

self.assertEqual(len(victim), 0)

test_clear_array_float(array.array('f', [1.0, 2.0, 3.0]))
test_clear_array_float(array.array('d', [1.0, 2.0, 3.0]))


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix null pointer dereference in :class:`!array.array.__setitem__` via
a user-defined ``__index__`` or ``__float__`` method which modifies the array
during index conversion.
61 changes: 60 additions & 1 deletion Modules/arraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,29 @@ Note that the basic Get and Set functions do NOT check that the index is
in bounds; that's the responsibility of the caller.
****************************************************************************/

/* Macro to check array buffer validity and bounds after calling
user-defined methods (like __index__ or __float__) that might modify
the array during the call.
*/
#define CHECK_ARRAY_BOUNDS(ap, i) \
do { \
if ((i) >= 0 && ((ap)->ob_item == NULL || (i) >= Py_SIZE((ap)))) { \
PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); \
return -1; \
} \
} while (0)

#define CHECK_ARRAY_BOUNDS_WITH_CLEANUP(ap, i, v, cleanup) \
do { \
if ((i) >= 0 && ((ap)->ob_item == NULL || (i) >= Py_SIZE((ap)))) { \
PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); \
if (cleanup) { \
Py_DECREF(v); \
} \
return -1; \
} \
} while (0)

static PyObject *
b_getitem(arrayobject *ap, Py_ssize_t i)
{
Expand All @@ -221,7 +244,10 @@ b_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
the overflow checking */
if (!PyArg_Parse(v, "h;array item must be integer", &x))
return -1;
else if (x < -128) {

CHECK_ARRAY_BOUNDS(ap, i);

if (x < -128) {
PyErr_SetString(PyExc_OverflowError,
"signed char is less than minimum");
return -1;
Expand Down Expand Up @@ -250,6 +276,9 @@ BB_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
/* 'B' == unsigned char, maps to PyArg_Parse's 'b' formatter */
if (!PyArg_Parse(v, "b;array item must be integer", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((unsigned char *)ap->ob_item)[i] = x;
return 0;
Expand Down Expand Up @@ -342,6 +371,9 @@ h_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
/* 'h' == signed short, maps to PyArg_Parse's 'h' formatter */
if (!PyArg_Parse(v, "h;array item must be integer", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((short *)ap->ob_item)[i] = x;
return 0;
Expand Down Expand Up @@ -371,6 +403,9 @@ HH_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
"unsigned short is greater than maximum");
return -1;
}

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((short *)ap->ob_item)[i] = (short)x;
return 0;
Expand All @@ -389,6 +424,9 @@ i_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
/* 'i' == signed int, maps to PyArg_Parse's 'i' formatter */
if (!PyArg_Parse(v, "i;array item must be integer", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((int *)ap->ob_item)[i] = x;
return 0;
Expand Down Expand Up @@ -429,6 +467,9 @@ II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
}
return -1;
}

CHECK_ARRAY_BOUNDS_WITH_CLEANUP(ap, i, v, do_decref);

if (i >= 0)
((unsigned int *)ap->ob_item)[i] = (unsigned int)x;

Expand All @@ -450,6 +491,9 @@ l_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
long x;
if (!PyArg_Parse(v, "l;array item must be integer", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((long *)ap->ob_item)[i] = x;
return 0;
Expand Down Expand Up @@ -481,6 +525,9 @@ LL_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
}
return -1;
}

CHECK_ARRAY_BOUNDS_WITH_CLEANUP(ap, i, v, do_decref);

if (i >= 0)
((unsigned long *)ap->ob_item)[i] = x;

Expand All @@ -502,6 +549,9 @@ q_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
long long x;
if (!PyArg_Parse(v, "L;array item must be integer", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((long long *)ap->ob_item)[i] = x;
return 0;
Expand Down Expand Up @@ -534,6 +584,9 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
}
return -1;
}

CHECK_ARRAY_BOUNDS_WITH_CLEANUP(ap, i, v, do_decref);

if (i >= 0)
((unsigned long long *)ap->ob_item)[i] = x;

Expand All @@ -555,6 +608,9 @@ f_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
float x;
if (!PyArg_Parse(v, "f;array item must be float", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((float *)ap->ob_item)[i] = x;
return 0;
Expand All @@ -572,6 +628,9 @@ d_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v)
double x;
if (!PyArg_Parse(v, "d;array item must be float", &x))
return -1;

CHECK_ARRAY_BOUNDS(ap, i);

if (i >= 0)
((double *)ap->ob_item)[i] = x;
return 0;
Expand Down
Loading