diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..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,6 +1713,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..d75a6a0035870a --- /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__`` method which modifies the array during index +conversion. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..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) { @@ -221,7 +235,12 @@ 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) { + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + + if (x < -128) { PyErr_SetString(PyExc_OverflowError, "signed char is less than minimum"); return -1; @@ -250,6 +269,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((unsigned char *)ap->ob_item)[i] = x; return 0; @@ -342,6 +366,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((short *)ap->ob_item)[i] = x; return 0; @@ -371,6 +400,11 @@ HH_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) "unsigned short is greater than maximum"); return -1; } + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((short *)ap->ob_item)[i] = (short)x; return 0; @@ -389,6 +423,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((int *)ap->ob_item)[i] = x; return 0; @@ -429,6 +468,14 @@ II_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + if (!array_check_bounds_after_user_call(ap, i)) { + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned int *)ap->ob_item)[i] = (unsigned int)x; @@ -450,6 +497,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((long *)ap->ob_item)[i] = x; return 0; @@ -481,6 +533,14 @@ LL_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + if (!array_check_bounds_after_user_call(ap, i)) { + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned long *)ap->ob_item)[i] = x; @@ -502,6 +562,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((long long *)ap->ob_item)[i] = x; return 0; @@ -534,6 +599,14 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) } return -1; } + + if (!array_check_bounds_after_user_call(ap, i)) { + if (do_decref) { + Py_DECREF(v); + } + return -1; + } + if (i >= 0) ((unsigned long long *)ap->ob_item)[i] = x; @@ -555,6 +628,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((float *)ap->ob_item)[i] = x; return 0; @@ -572,6 +650,11 @@ 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; + + if (!array_check_bounds_after_user_call(ap, i)) { + return -1; + } + if (i >= 0) ((double *)ap->ob_item)[i] = x; return 0;