From 71fc725d4c7f1ff40771b5c7e5f67f7279237142 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 15 Dec 2025 02:05:24 +0900 Subject: [PATCH 1/9] Fix null pointer dereference in array.__setitem__ via re-entrant __index__ --- Lib/test/test_array.py | 17 +++++++++++++++++ ...25-12-15-02-02-45.gh-issue-142555.EC9QN_.rst | 3 +++ Modules/arraymodule.c | 12 +++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..379b346697663a 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1680,6 +1680,23 @@ def test_gh_128961(self): it.__setstate__(0) self.assertRaises(StopIteration, next, it) + def test_gh_142555(self): + # Test for null pointer dereference in array.__setitem__ + # via re-entrant __index__. + victim = array.array('b', [0] * 64) + + class EvilIndex: + def __index__(self): + # Re-entrant mutation: shrink 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) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst new file mode 100644 index 00000000000000..218a252db982da --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/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 +an user-defined ``__index__`` method which modify the array during index +conversion. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..aa3464758dc22e 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -221,7 +221,17 @@ 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 buffer validity after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && ap->ob_item == NULL) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + + if (x < -128) { PyErr_SetString(PyExc_OverflowError, "signed char is less than minimum"); return -1; From 6c437d52c01971e3ac41518093f9fc74a92a0702 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 15 Dec 2025 21:46:40 +0900 Subject: [PATCH 2/9] Update Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .../2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst index 218a252db982da..d75a6a0035870a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst @@ -1,3 +1,3 @@ -Fix null pointer dereference in :class:`array.array.__setitem__` via -an user-defined ``__index__`` method which modify the array during index +Fix null pointer dereference in :class:`!array.array.__setitem__` via +a user-defined ``__index__`` method which modifies the array during index conversion. From e3c728a0db7ec2c5f6066be0c5c5b54d42a16a3b Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 15 Dec 2025 21:48:51 +0900 Subject: [PATCH 3/9] Move the news entry to library folder --- .../2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/{Core_and_Builtins => Library}/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst (100%) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/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 similarity index 100% rename from Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst rename to Misc/NEWS.d/next/Library/2025-12-15-02-02-45.gh-issue-142555.EC9QN_.rst From 67e98b5c73066944d860d2e65720835b3c93b90c Mon Sep 17 00:00:00 2001 From: AN Long Date: Thu, 18 Dec 2025 01:36:41 +0900 Subject: [PATCH 4/9] Also check if the ob_item's size is changed by user's code --- Lib/test/test_array.py | 13 +++++++++++++ Modules/arraymodule.c | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 379b346697663a..cee793590d04db 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1697,6 +1697,19 @@ def __index__(self): self.assertEqual(len(victim), 0) + # Test case where array is shrunk but not completely cleared + victim2 = array.array('b', [1, 2, 3]) + + class ShrinkIndex: + def __index__(self): + # Pop two elements, making array size 1, so index 1 is out of bounds + victim2.pop() + victim2.pop() + return 0 + + with self.assertRaises(IndexError): + victim2[1] = ShrinkIndex() + if __name__ == "__main__": unittest.main() diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index aa3464758dc22e..f92a1968369832 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -222,10 +222,10 @@ b_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "h;array item must be integer", &x)) return -1; - /* Check buffer validity after PyArg_Parse which may call user-defined + /* Check buffer validity and bounds after PyArg_Parse which may call user-defined * __index__ on v, which might modify the array buffer. See gh-142555. */ - if (i >= 0 && ap->ob_item == NULL) { + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); return -1; From ad18d48e7bb7657f1093308806512d633dc6d6d0 Mon Sep 17 00:00:00 2001 From: AN Long Date: Thu, 18 Dec 2025 02:09:06 +0900 Subject: [PATCH 5/9] Add check on more *_setitem methods --- Lib/test/test_array.py | 62 +++++++++++++++++++------------- Modules/arraymodule.c | 80 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index cee793590d04db..8be5deb91ea433 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1683,32 +1683,44 @@ def test_gh_128961(self): def test_gh_142555(self): # Test for null pointer dereference in array.__setitem__ # via re-entrant __index__. - victim = array.array('b', [0] * 64) - class EvilIndex: - def __index__(self): - # Re-entrant mutation: shrink 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) - - # Test case where array is shrunk but not completely cleared - victim2 = array.array('b', [1, 2, 3]) - - class ShrinkIndex: - def __index__(self): - # Pop two elements, making array size 1, so index 1 is out of bounds - victim2.pop() - victim2.pop() - return 0 - - with self.assertRaises(IndexError): - victim2[1] = ShrinkIndex() + def test_clear_array(victim): + """Test array clearing scenario""" + 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): + """Test array shrinking scenario""" + class ShrinkIndex: + def __index__(self): + # Pop two elements, making array size 1, so index 1 is out of bounds + victim.pop() + victim.pop() + return 0 + + with self.assertRaises(IndexError): + victim[1] = ShrinkIndex() # Original index 1 should now be out of bounds + + # Test various array types + 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('f', [1.0, 2.0, 3.0])) + test_clear_array(array.array('d', [1.0, 2.0, 3.0])) if __name__ == "__main__": diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index f92a1968369832..2fbc674ed488fb 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -260,6 +260,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((unsigned char *)ap->ob_item)[i] = x; return 0; @@ -352,6 +362,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((short *)ap->ob_item)[i] = x; return 0; @@ -381,6 +401,16 @@ HH_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) "unsigned short is greater than maximum"); return -1; } + + /* Check buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((short *)ap->ob_item)[i] = (short)x; return 0; @@ -399,6 +429,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((int *)ap->ob_item)[i] = x; return 0; @@ -460,6 +500,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((long *)ap->ob_item)[i] = x; return 0; @@ -512,6 +562,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((long long *)ap->ob_item)[i] = x; return 0; @@ -565,6 +625,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((float *)ap->ob_item)[i] = x; return 0; @@ -582,6 +652,16 @@ 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 buffer validity and bounds after PyArg_Parse which may call user-defined + * __index__ on v, which might modify the array buffer. See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return -1; + } + if (i >= 0) ((double *)ap->ob_item)[i] = x; return 0; From c408c577b032d3be720d97170d2d572ec52ce23b Mon Sep 17 00:00:00 2001 From: AN Long Date: Fri, 19 Dec 2025 01:10:03 +0900 Subject: [PATCH 6/9] Check other setitem methods --- Lib/test/test_array.py | 23 +++++++++++++++++++++-- Modules/arraymodule.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 8be5deb91ea433..9937bd0bf4b658 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1710,6 +1710,20 @@ def __index__(self): with self.assertRaises(IndexError): victim[1] = ShrinkIndex() # Original index 1 should now be out of bounds + 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 various array types test_clear_array(array.array('b', [0] * 64)) test_shrink_array(array.array('b', [1, 2, 3])) @@ -1719,8 +1733,13 @@ def __index__(self): 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('f', [1.0, 2.0, 3.0])) - test_clear_array(array.array('d', [1.0, 2.0, 3.0])) + 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 float arrays with __float__ method + 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__": diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 2fbc674ed488fb..c23ceaff8bc874 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -479,6 +479,20 @@ II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + /* Check buffer validity and bounds after potential user code calls + * (_PyNumber_Index and PyLong_AsUnsignedLong may modify the array buffer). + * See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned int *)ap->ob_item)[i] = (unsigned int)x; @@ -541,6 +555,20 @@ LL_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + /* Check buffer validity and bounds after potential user code calls + * (_PyNumber_Index and PyLong_AsUnsignedLong may modify the array buffer). + * See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned long *)ap->ob_item)[i] = x; @@ -604,6 +632,20 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + /* Check buffer validity and bounds after potential user code calls + * (_PyNumber_Index and PyLong_AsUnsignedLongLong may modify the array buffer). + * See gh-142555. + */ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned long long *)ap->ob_item)[i] = x; From 95e4eec1792ca556a649a05729435ffba5a94fee Mon Sep 17 00:00:00 2001 From: AN Long Date: Fri, 19 Dec 2025 01:24:16 +0900 Subject: [PATCH 7/9] Reduce the comments --- Modules/arraymodule.c | 51 ++++++++++--------------------------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index c23ceaff8bc874..1bd1f3484938c0 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -222,9 +222,7 @@ b_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "h;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -261,9 +259,7 @@ BB_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "b;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -363,9 +359,7 @@ h_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "h;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -402,9 +396,7 @@ HH_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -430,9 +422,7 @@ i_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "i;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -480,10 +470,7 @@ II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - /* Check buffer validity and bounds after potential user code calls - * (_PyNumber_Index and PyLong_AsUnsignedLong may modify the array buffer). - * See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -515,9 +502,7 @@ l_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "l;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -556,10 +541,7 @@ LL_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - /* Check buffer validity and bounds after potential user code calls - * (_PyNumber_Index and PyLong_AsUnsignedLong may modify the array buffer). - * See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -591,9 +573,7 @@ q_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "L;array item must be integer", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -633,10 +613,7 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - /* Check buffer validity and bounds after potential user code calls - * (_PyNumber_Index and PyLong_AsUnsignedLongLong may modify the array buffer). - * See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -668,9 +645,7 @@ f_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "f;array item must be float", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); @@ -695,9 +670,7 @@ d_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "d;array item must be float", &x)) return -1; - /* Check buffer validity and bounds after PyArg_Parse which may call user-defined - * __index__ on v, which might modify the array buffer. See gh-142555. - */ + // Check buffer validity and bounds after call user-defined method. if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { PyErr_SetString(PyExc_IndexError, "array assignment index out of range"); From fc6f987fa84bc3401d8514bd00626fbd1387769f Mon Sep 17 00:00:00 2001 From: AN Long Date: Fri, 19 Dec 2025 01:35:08 +0900 Subject: [PATCH 8/9] Re-order the test --- Lib/test/test_array.py | 144 +++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 57 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 9937bd0bf4b658..1e85535b46e404 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -26,16 +26,20 @@ class ArraySubclass(array.array): pass + class ArraySubclassWithKwargs(array.array): def __init__(self, typecode, newarg=None): array.array.__init__(self) + typecodes = 'uwbBhHiIlLfdqQ' + class MiscTest(unittest.TestCase): def test_array_is_sequence(self): - self.assertIsInstance(array.array("B"), collections.abc.MutableSequence) + self.assertIsInstance(array.array( + "B"), collections.abc.MutableSequence) self.assertIsInstance(array.array("B"), collections.abc.Reversible) def test_bad_constructor(self): @@ -140,32 +144,32 @@ def test_numbers(self): (['h', 'i', 'l'], SIGNED_INT16_BE, '>hhh', [-0x8000, 0x7fff, 0]), (['I', 'L'], UNSIGNED_INT32_LE, 'IIII', - [1<<31, (1<<31)-1, 0, (1<<32)-1]), + [1 << 31, (1 << 31)-1, 0, (1 << 32)-1]), (['i', 'l'], SIGNED_INT32_LE, 'iii', - [-1<<31, (1<<31)-1, 0]), + [-1 << 31, (1 << 31)-1, 0]), (['L'], UNSIGNED_INT64_LE, 'QQQQ', - [1<<31, (1<<31)-1, 0, (1<<32)-1]), + [1 << 31, (1 << 31)-1, 0, (1 << 32)-1]), (['l'], SIGNED_INT64_LE, 'qqq', - [-1<<31, (1<<31)-1, 0]), + [-1 << 31, (1 << 31)-1, 0]), # The following tests for INT64 will raise an OverflowError # when run on a 32-bit machine. The tests are simply skipped # in that case. (['L'], UNSIGNED_INT64_LE, 'QQQQ', - [1<<63, (1<<63)-1, 0, (1<<64)-1]), + [1 << 63, (1 << 63)-1, 0, (1 << 64)-1]), (['l'], SIGNED_INT64_LE, 'qqq', - [-1<<63, (1<<63)-1, 0]), + [-1 << 63, (1 << 63)-1, 0]), (['f'], IEEE_754_FLOAT_LE, 'ffff', @@ -186,7 +190,7 @@ def test_numbers(self): b = array_reconstructor( array.array, typecode, mformat_code, arraystr) self.assertEqual(a, b, - msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) + msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) def test_unicode(self): teststr = "Bonne Journ\xe9e \U0002030a\U00020347" @@ -203,7 +207,7 @@ def test_unicode(self): b = array_reconstructor( array.array, c, mformat_code, teststr.encode(encoding)) self.assertEqual(a, b, - msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) + msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) class BaseTest: @@ -264,7 +268,7 @@ def test_byteswap(self): if a.itemsize in (1, 2, 4, 8): b = array.array(self.typecode, example) b.byteswap() - if a.itemsize==1: + if a.itemsize == 1: self.assertEqual(a, b) else: self.assertNotEqual(a, b) @@ -537,7 +541,7 @@ def test_tofrombytes(self): c = array.array(self.typecode, bytearray(a.tobytes())) self.assertEqual(a, b) self.assertEqual(a, c) - if a.itemsize>1: + if a.itemsize > 1: self.assertRaises(ValueError, b.frombytes, b"x") def test_fromarray(self): @@ -906,7 +910,8 @@ def test_setslice(self): a[1:0] = a self.assertEqual( a, - array.array(self.typecode, self.example[:1] + self.example + self.example[1:]) + array.array(self.typecode, + self.example[:1] + self.example + self.example[1:]) ) a = array.array(self.typecode, self.example) @@ -1005,7 +1010,8 @@ def test_pop(self): self.assertEntryEqual(a.pop(1), self.example[2]) self.assertEqual( a, - array.array(self.typecode, self.example[1:2]+self.example[3:]+self.example) + array.array(self.typecode, + self.example[1:2]+self.example[3:]+self.example) ) self.assertEntryEqual(a.pop(0), self.example[1]) self.assertEntryEqual(a.pop(), self.example[-1]) @@ -1203,6 +1209,7 @@ def test_free_after_iterating(self): support.check_free_after_iterating(self, reversed, array.array, (self.typecode,)) + class StringTest(BaseTest): def test_setitem(self): @@ -1210,6 +1217,7 @@ def test_setitem(self): a = array.array(self.typecode, self.example) self.assertRaises(TypeError, a.__setitem__, 0, self.example[:2]) + class UnicodeTest(StringTest, unittest.TestCase): typecode = 'u' example = '\x01\u263a\x00\ufeff' @@ -1274,41 +1282,44 @@ class NumberTest(BaseTest): def test_extslice(self): a = array.array(self.typecode, range(5)) self.assertEqual(a[::], a) - self.assertEqual(a[::2], array.array(self.typecode, [0,2,4])) - self.assertEqual(a[1::2], array.array(self.typecode, [1,3])) - self.assertEqual(a[::-1], array.array(self.typecode, [4,3,2,1,0])) - self.assertEqual(a[::-2], array.array(self.typecode, [4,2,0])) - self.assertEqual(a[3::-2], array.array(self.typecode, [3,1])) + self.assertEqual(a[::2], array.array(self.typecode, [0, 2, 4])) + self.assertEqual(a[1::2], array.array(self.typecode, [1, 3])) + self.assertEqual(a[::-1], array.array(self.typecode, [4, 3, 2, 1, 0])) + self.assertEqual(a[::-2], array.array(self.typecode, [4, 2, 0])) + self.assertEqual(a[3::-2], array.array(self.typecode, [3, 1])) self.assertEqual(a[-100:100:], a) self.assertEqual(a[100:-100:-1], a[::-1]) - self.assertEqual(a[-100:100:2], array.array(self.typecode, [0,2,4])) + self.assertEqual(a[-100:100:2], array.array(self.typecode, [0, 2, 4])) self.assertEqual(a[1000:2000:2], array.array(self.typecode, [])) self.assertEqual(a[-1000:-2000:-2], array.array(self.typecode, [])) def test_delslice(self): a = array.array(self.typecode, range(5)) del a[::2] - self.assertEqual(a, array.array(self.typecode, [1,3])) + self.assertEqual(a, array.array(self.typecode, [1, 3])) a = array.array(self.typecode, range(5)) del a[1::2] - self.assertEqual(a, array.array(self.typecode, [0,2,4])) + self.assertEqual(a, array.array(self.typecode, [0, 2, 4])) a = array.array(self.typecode, range(5)) del a[1::-2] - self.assertEqual(a, array.array(self.typecode, [0,2,3,4])) + self.assertEqual(a, array.array(self.typecode, [0, 2, 3, 4])) a = array.array(self.typecode, range(10)) del a[::1000] - self.assertEqual(a, array.array(self.typecode, [1,2,3,4,5,6,7,8,9])) + self.assertEqual(a, array.array( + self.typecode, [1, 2, 3, 4, 5, 6, 7, 8, 9])) # test issue7788 a = array.array(self.typecode, range(10)) - del a[9::1<<333] + del a[9::1 << 333] def test_assignment(self): a = array.array(self.typecode, range(10)) a[::2] = array.array(self.typecode, [42]*5) - self.assertEqual(a, array.array(self.typecode, [42, 1, 42, 3, 42, 5, 42, 7, 42, 9])) + self.assertEqual(a, array.array( + self.typecode, [42, 1, 42, 3, 42, 5, 42, 7, 42, 9])) a = array.array(self.typecode, range(10)) a[::-4] = array.array(self.typecode, [10]*3) - self.assertEqual(a, array.array(self.typecode, [0, 10, 2, 3, 4, 10, 6, 7, 8 ,10])) + self.assertEqual(a, array.array( + self.typecode, [0, 10, 2, 3, 4, 10, 6, 7, 8, 10])) a = array.array(self.typecode, range(4)) a[::-1] = a self.assertEqual(a, array.array(self.typecode, [3, 2, 1, 0])) @@ -1317,7 +1328,7 @@ def test_assignment(self): c = a[:] ins = array.array(self.typecode, range(2)) a[2:3] = ins - b[slice(2,3)] = ins + b[slice(2, 3)] = ins c[2:3:] = ins def test_iterationcontains(self): @@ -1345,6 +1356,7 @@ def check_overflow(self, lower, upper): def test_subclassing(self): typecode = self.typecode + class ExaggeratingArray(array.array): __slots__ = ['offset'] @@ -1367,6 +1379,7 @@ def test_frombytearray(self): b = array.array(self.typecode, a) self.assertEqual(a, b) + class IntegerNumberTest(NumberTest): def test_type_error(self): a = array.array(self.typecode) @@ -1376,18 +1389,24 @@ def test_type_error(self): with self.assertRaises(TypeError): a[0] = 42.0 + class Intable: def __init__(self, num): self._num = num + def __index__(self): return self._num + def __int__(self): return self._num + def __sub__(self, other): return Intable(int(self) - int(other)) + def __add__(self, other): return Intable(int(self) + int(other)) + class SignedNumberTest(IntegerNumberTest): example = [-1, 0, 1, 42, 0x7f] smallerexample = [-1, 0, 1, 42, 0x7e] @@ -1401,6 +1420,7 @@ def test_overflow(self): self.check_overflow(lower, upper) self.check_overflow(Intable(lower), Intable(upper)) + class UnsignedNumberTest(IntegerNumberTest): example = [0, 1, 17, 23, 42, 0xff] smallerexample = [0, 1, 17, 23, 42, 0xfe] @@ -1436,42 +1456,52 @@ class ByteTest(SignedNumberTest, unittest.TestCase): typecode = 'b' minitemsize = 1 + class UnsignedByteTest(UnsignedNumberTest, unittest.TestCase): typecode = 'B' minitemsize = 1 + class ShortTest(SignedNumberTest, unittest.TestCase): typecode = 'h' minitemsize = 2 + class UnsignedShortTest(UnsignedNumberTest, unittest.TestCase): typecode = 'H' minitemsize = 2 + class IntTest(SignedNumberTest, unittest.TestCase): typecode = 'i' minitemsize = 2 + class UnsignedIntTest(UnsignedNumberTest, unittest.TestCase): typecode = 'I' minitemsize = 2 + class LongTest(SignedNumberTest, unittest.TestCase): typecode = 'l' minitemsize = 4 + class UnsignedLongTest(UnsignedNumberTest, unittest.TestCase): typecode = 'L' minitemsize = 4 + class LongLongTest(SignedNumberTest, unittest.TestCase): typecode = 'q' minitemsize = 8 + class UnsignedLongLongTest(UnsignedNumberTest, unittest.TestCase): typecode = 'Q' minitemsize = 8 + class FPTest(NumberTest): example = [-42.0, 0, 42, 1e5, -1e10] smallerexample = [-42.0, 0, 42, 1e5, -2e10] @@ -1497,7 +1527,7 @@ def test_byteswap(self): if a.itemsize in (1, 2, 4, 8): b = array.array(self.typecode, self.example) b.byteswap() - if a.itemsize==1: + if a.itemsize == 1: self.assertEqual(a, b) else: # On alphas treating the byte swapped bit patterns as @@ -1507,10 +1537,12 @@ def test_byteswap(self): b.byteswap() self.assertEqual(a, b) + class FloatTest(FPTest, unittest.TestCase): typecode = 'f' minitemsize = 4 + class DoubleTest(FPTest, unittest.TestCase): typecode = 'd' minitemsize = 8 @@ -1524,7 +1556,7 @@ def test_alloc_overflow(self): pass else: self.fail("Array of size > maxsize created - MemoryError expected") - b = array.array('d', [ 2.71828183, 3.14159265, -1]) + b = array.array('d', [2.71828183, 3.14159265, -1]) try: b * (maxsize//3 + 1) except MemoryError: @@ -1538,7 +1570,8 @@ class LargeArrayTest(unittest.TestCase): def example(self, size): # We assess a base memuse of <=2.125 for constructing this array - base = array.array(self.typecode, [0, 1, 2, 3, 4, 5, 6, 7]) * (size // 8) + base = array.array( + self.typecode, [0, 1, 2, 3, 4, 5, 6, 7]) * (size // 8) base += array.array(self.typecode, [99]*(size % 8) + [8, 9, 10, 11]) return base @@ -1680,12 +1713,11 @@ def test_gh_128961(self): it.__setstate__(0) self.assertRaises(StopIteration, next, it) - def test_gh_142555(self): - # Test for null pointer dereference in array.__setitem__ - # via re-entrant __index__. + 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): - """Test array clearing scenario""" class EvilIndex: def __index__(self): # Re-entrant mutation: clear the array while __setitem__ @@ -1699,16 +1731,28 @@ def __index__(self): self.assertEqual(len(victim), 0) def test_shrink_array(victim): - """Test array shrinking scenario""" class ShrinkIndex: def __index__(self): - # Pop two elements, making array size 1, so index 1 is out of bounds + # 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() # Original index 1 should now be out of bounds + 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""" @@ -1724,20 +1768,6 @@ def __float__(self): self.assertEqual(len(victim), 0) - # Test various array types - 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])) - - # Test float arrays with __float__ method 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])) From c8cbd28bac06fd37f88634bae03bf4f17c6fafc0 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 20 Dec 2025 01:25:50 +0900 Subject: [PATCH 9/9] Refactor the array check codes to a function --- Modules/arraymodule.c | 74 +++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 1bd1f3484938c0..4f63e4c67afe27 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -205,6 +205,20 @@ Note that the basic Get and Set functions do NOT check that the index is in bounds; that's the responsibility of the caller. ****************************************************************************/ +/* Check array buffer validity and bounds after calling user-defined methods + (like __index__ or __float__) that might modify the array during the call. + Returns false on error, true on success. */ +static inline bool +array_check_bounds_after_user_call(arrayobject *ap, Py_ssize_t i) +{ + if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { + PyErr_SetString(PyExc_IndexError, + "array assignment index out of range"); + return false; + } + return true; +} + static PyObject * b_getitem(arrayobject *ap, Py_ssize_t i) { @@ -222,10 +236,7 @@ b_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "h;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -259,10 +270,7 @@ BB_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "b;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -359,10 +367,7 @@ h_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "h;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -396,10 +401,7 @@ HH_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -422,10 +424,7 @@ i_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "i;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -470,10 +469,7 @@ II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { if (do_decref) { Py_DECREF(v); } @@ -502,10 +498,7 @@ l_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "l;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -541,10 +534,7 @@ LL_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { if (do_decref) { Py_DECREF(v); } @@ -573,10 +563,7 @@ q_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "L;array item must be integer", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -613,10 +600,7 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return -1; } - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { if (do_decref) { Py_DECREF(v); } @@ -645,10 +629,7 @@ f_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "f;array item must be float", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; } @@ -670,10 +651,7 @@ d_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) if (!PyArg_Parse(v, "d;array item must be float", &x)) return -1; - // Check buffer validity and bounds after call user-defined method. - if (i >= 0 && (ap->ob_item == NULL || i >= Py_SIZE(ap))) { - PyErr_SetString(PyExc_IndexError, - "array assignment index out of range"); + if (!array_check_bounds_after_user_call(ap, i)) { return -1; }