diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..d082a9d51a9246 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -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() diff --git a/Misc/NEWS.d/next/Library/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst b/Misc/NEWS.d/next/Library/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst new file mode 100644 index 00000000000000..4b25983d856111 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst @@ -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. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..b8b72323011ac9 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -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) { @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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;