From d17521c769b59c602ec5028a57fe390f4a3da60a Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 16 Jan 2026 19:29:15 +0530 Subject: [PATCH 1/6] gh-143005: prevent incompatible __class__ reassignment for ctypes arrays --- Lib/test/test_ctypes/test_arrays.py | 8 ++++ Modules/_ctypes/_ctypes.c | 58 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 7f1f6cf58402c9..52eefcb4d0395b 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -102,6 +102,14 @@ def test_simple(self): # cannot delete items 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_step_overflow(self): a = (c_int * 5)() diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 563e95a762599b..58a639be308d8f 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,41 @@ 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_CompareWithASCIIString(key, "__class__") == 0) + { + ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); + StgInfo *old_info; + StgInfo *new_info; + + if (PyStgInfo_FromObject(st, self, &old_info) < 0) { + return -1; + } + if (PyStgInfo_FromType(st, value, &new_info) < 0) { + return -1; + } + + /* Only care about array → array */ + if (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 -1; + } + } + } + + return PyObject_GenericSetAttr(self, key, value); +} + static PyObject * Array_item_lock_held(PyObject *myself, Py_ssize_t index) { @@ -5310,6 +5367,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}, }; From 670c7a17589e13d6eab19e980b3f84c21642d643 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 16 Jan 2026 19:38:55 +0530 Subject: [PATCH 2/6] gh-143005: trim trailing whitespace --- Lib/test/test_ctypes/test_arrays.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 52eefcb4d0395b..6002cf88a9a0f4 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -102,12 +102,12 @@ def test_simple(self): # cannot delete items 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 From 32fac412e380ea225ba91806eb244f71db344887 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 17 Jan 2026 16:37:03 +0530 Subject: [PATCH 3/6] News added --- .../next/Library/2026-01-17-16-35-57.gh-issue-143005.WKf6GL.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-17-16-35-57.gh-issue-143005.WKf6GL.rst 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. From 56903c546de77e0b81ed94734eb91284732b62b7 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Tue, 20 Jan 2026 12:41:57 +0530 Subject: [PATCH 4/6] gh-143005: disallow incompatible __class__ reassignment for ctypes arrays --- Lib/test/test_ctypes/test_arrays.py | 42 ++++++++++++++++++++++++++++- Modules/_ctypes/_ctypes.c | 17 +++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 6002cf88a9a0f4..4c881546757678 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -4,7 +4,7 @@ from ctypes import (Structure, Array, ARRAY, sizeof, addressof, create_string_buffer, create_unicode_buffer, c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, - c_long, c_ulonglong, c_float, c_double, c_longdouble) + c_long, c_ulonglong, c_float, c_double, c_longdouble, POINTER) from test.support import bigmemtest, _2G, threading_helper, Py_GIL_DISABLED from ._support import (_CData, PyCArrayType, Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYPE) @@ -111,6 +111,46 @@ def test_ctypes_array_class_assignment_incompatible(self): with self.assertRaises(TypeError): x.__class__ = B + def test_ctypes_array_class_assignment_abstract_target(self): + class AbstractArray(Array): + pass + A = c_int * 3 + a = A() + + with self.assertRaises(TypeError): + a.__class__ = AbstractArray + + def test_ctypes_array_class_assignment_non_array_instance(self): + p = POINTER(c_int)() + A = c_int * 3 + + with self.assertRaises(TypeError): + p.__class__ = A + + 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_step_overflow(self): a = (c_int * 5)() a[3::sys.maxsize] = (1,) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 58a639be308d8f..51f0d687afe40c 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5018,11 +5018,11 @@ static int PyCArray_setattro(PyObject *self, PyObject *key, PyObject *value) { if (PyUnicode_Check(key) && - PyUnicode_CompareWithASCIIString(key, "__class__") == 0) + _PyUnicode_EqualToASCIIString(key, "__class__")) { ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); - StgInfo *old_info; - StgInfo *new_info; + StgInfo *old_info = NULL; + StgInfo *new_info = NULL; if (PyStgInfo_FromObject(st, self, &old_info) < 0) { return -1; @@ -5031,7 +5031,16 @@ PyCArray_setattro(PyObject *self, PyObject *key, PyObject *value) return -1; } - /* Only care about array → array */ + /* 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) { if (old_info->length != new_info->length || old_info->size != new_info->size || From 5bb9dfb12e3a6f989dac4e74d7a90e0012bc0d22 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Tue, 20 Jan 2026 13:03:29 +0530 Subject: [PATCH 5/6] gh-143005: fix ctypes array __class__ reassignment tests --- Lib/test/test_ctypes/test_arrays.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 4c881546757678..d7a283a53f9bbc 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -111,21 +111,17 @@ def test_ctypes_array_class_assignment_incompatible(self): with self.assertRaises(TypeError): x.__class__ = B - def test_ctypes_array_class_assignment_abstract_target(self): - class AbstractArray(Array): - pass + 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__ = AbstractArray + a.__class__ = OtherArray - def test_ctypes_array_class_assignment_non_array_instance(self): - p = POINTER(c_int)() - A = c_int * 3 - - with self.assertRaises(TypeError): - p.__class__ = A def test_ctypes_array_class_assignment_zero_length(self): A = c_long * 0 From 8abda121533d4fd8a92721a396738a406655b8d1 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Tue, 20 Jan 2026 13:08:33 +0530 Subject: [PATCH 6/6] gh-143005: apply prek fixes --- Lib/test/test_ctypes/test_arrays.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index d7a283a53f9bbc..fe6fd23c451d89 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -4,7 +4,7 @@ from ctypes import (Structure, Array, ARRAY, sizeof, addressof, create_string_buffer, create_unicode_buffer, c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, - c_long, c_ulonglong, c_float, c_double, c_longdouble, POINTER) + c_long, c_ulonglong, c_float, c_double, c_longdouble) from test.support import bigmemtest, _2G, threading_helper, Py_GIL_DISABLED from ._support import (_CData, PyCArrayType, Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYPE)