diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 7f1f6cf58402c9..b76e76a22c6c48 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -103,6 +103,103 @@ def test_simple(self): with self.assertRaises(TypeError): del ca[0] + def test_ctypes_array_class_assignment_incompatible(self): + A = c_long * 3 + B = c_long * 5 + x = A(1, 2, 3) + + with self.assertRaises(TypeError): + x.__class__ = B + + def test_ctypes_array_class_assignment_incompatible_target(self): + A = c_int * 3 + class OtherArray(Array): + _type_ = c_int + _length_ = 4 # incompatible length + + a = A() + + with self.assertRaises(TypeError): + a.__class__ = OtherArray + + + def test_ctypes_array_class_assignment_zero_length(self): + A = c_long * 0 + B = c_long * 1 + a = A() + + with self.assertRaises(TypeError): + a.__class__ = B + + def test_ctypes_array_class_assignment_incompatible_element_type(self): + A = c_int * 3 + B = c_double * 3 + a = A() + + with self.assertRaises(TypeError): + a.__class__ = B + + def test_ctypes_array_class_assignment_signed_unsigned(self): + A = c_long * 3 + B = c_ulonglong * 3 + a = A() + + with self.assertRaises(TypeError): + a.__class__ = B + + def test_ctypes_array_class_assignment_compatible(self): + A = c_int * 3 + class SameArray(Array): + _type_ = c_int + _length_ = 3 + a = A(1, 2, 3) + a.__class__ = SameArray + + def test_ctypes_array_class_assignment_non_ctypes_target(self): + A = c_int * 3 + a = A() + class Dummy: + pass + + with self.assertRaises(TypeError): + a.__class__ = Dummy + + def test_ctypes_array_class_assignment_abstract_target(self): + A = c_int * 3 + a = A() + AbstractArray = PyCArrayType.__new__(PyCArrayType, "AbstractArray", (Array,), {}) + + with self.assertRaises(TypeError): + a.__class__ = AbstractArray + + def test_ctypes_array_class_assignment_same_size_ints(self): + if sizeof(c_int) != sizeof(c_long): + self.skipTest("sizes differ on this platform") + A = c_int * 3 + B = c_long * 3 + a = A(1, 2, 3) + a.__class__ = B + + def test_ctypes_array_class_assignment_structs(self): + class S1(Structure): + _fields_ = [("x", c_int)] + A = S1 * 2 + B = S1 * 2 + a = A() + a.__class__ = B + + def test_ctypes_array_class_assignment_pointer_arrays(self): + from ctypes import POINTER + A = POINTER(c_int) * 2 + B = POINTER(c_int) * 2 + a = A() + a.__class__ = B + + def test_ctypes_array_from_param_incompatible(self): + A = c_int * 3 + with self.assertRaises(TypeError): + A.from_param(object()) + def test_step_overflow(self): a = (c_int * 5)() a[3::sys.maxsize] = (1,) diff --git a/Misc/NEWS.d/next/Library/2026-01-17-16-35-57.gh-issue-143005.WKf6GL.rst b/Misc/NEWS.d/next/Library/2026-01-17-16-35-57.gh-issue-143005.WKf6GL.rst new file mode 100644 index 00000000000000..64895e225585fb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-17-16-35-57.gh-issue-143005.WKf6GL.rst @@ -0,0 +1,2 @@ +Fix a memory safety issue in ctypes arrays by rejecting ``__class__`` +assignment to incompatible array types. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 563e95a762599b..7c66e714ac4739 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1097,6 +1097,28 @@ CDataType_from_param_impl(PyObject *type, PyTypeObject *cls, PyObject *value) return Py_NewRef(value); } ctypes_state *st = get_module_state_by_class(cls); + + /* Disallow incompatible ctypes array __class__ reassignment */ + StgInfo *old_info = NULL; + StgInfo *new_info = NULL; + if (PyStgInfo_FromObject(st, value, &old_info) == 0 && + PyStgInfo_FromType(st, type, &new_info) == 0 && + old_info != NULL && + new_info != NULL && + old_info->length >= 0 && + new_info->length >= 0) + { + if (old_info->length != new_info->length || + old_info->size != new_info->size || + old_info->proto != new_info->proto) + { + PyErr_SetString( + PyExc_TypeError, + "cannot assign incompatible ctypes array type" + ); + return NULL; + } + } if (PyCArg_CheckExact(st, value)) { PyCArgObject *p = (PyCArgObject *)value; PyObject *ob = p->obj; @@ -4992,6 +5014,55 @@ Array_init(PyObject *self, PyObject *args, PyObject *kw) return 0; } +static int +PyCArray_setattro(PyObject *self, PyObject *key, PyObject *value) +{ + if (PyUnicode_Check(key) && + _PyUnicode_EqualToASCIIString(key, "__class__")) + { + ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); + StgInfo *old_info = NULL; + StgInfo *new_info = NULL; + + if (PyStgInfo_FromObject(st, self, &old_info) < 0) { + return -1; + } + if (PyStgInfo_FromType(st, value, &new_info) < 0) { + return -1; + } + + /* If one side has no storage info, disallow */ + if (old_info == NULL || new_info == NULL) { + PyErr_SetString( + PyExc_TypeError, + "cannot assign incompatible ctypes array type" + ); + return -1; + } + + /* Only care about array to array */ + if (old_info->length < 0 || new_info->length < 0) { + PyErr_SetString( + PyExc_TypeError, + "cannot assign incompatible ctypes array type"); + return -1; + } + + /* Must match layout */ + if (old_info->length != new_info->length || + old_info->size != new_info->size || + old_info->proto != new_info->proto) + { + PyErr_SetString( + PyExc_TypeError, + "cannot assign incompatible ctypes array type"); + return -1; + } + } + + return PyObject_GenericSetAttr(self, key, value); +} + static PyObject * Array_item_lock_held(PyObject *myself, Py_ssize_t index) { @@ -5310,6 +5381,7 @@ static PyType_Slot pycarray_slots[] = { {Py_mp_length, Array_length}, {Py_mp_subscript, Array_subscript}, {Py_mp_ass_subscript, Array_ass_subscript}, + {Py_tp_setattro, PyCArray_setattro}, {0, NULL}, };