From d17521c769b59c602ec5028a57fe390f4a3da60a Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 16 Jan 2026 19:29:15 +0530 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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) From 4dc18906b62cf550a65a0234a53f0eba68e4a1d8 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 12 Feb 2026 23:12:37 +0530 Subject: [PATCH 07/10] Added all test cases --- Lib/test/test_ctypes/test_arrays.py | 57 ++++++++++++++++++++++++++++- Modules/_ctypes/_ctypes.c | 27 ++++++++------ 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index fe6fd23c451d89..be044379a3845f 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) @@ -147,6 +147,61 @@ def test_ctypes_array_class_assignment_signed_unsigned(self): 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)] + class S2(Structure): + _fields_ = [("x", c_int)] + A = S1 * 2 + B = S2 * 2 + a = A() + a.__class__ = B + + def test_ctypes_array_class_assignment_pointer_arrays(self): + 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 + B = c_double * 3 + with self.assertRaises(TypeError): + A.from_param(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 51f0d687afe40c..7c66e714ac4739 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5041,17 +5041,22 @@ PyCArray_setattro(PyObject *self, PyObject *key, PyObject *value) } /* 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 || - old_info->proto != new_info->proto) - { - PyErr_SetString( - PyExc_TypeError, - "cannot assign incompatible ctypes array type" - ); - return -1; - } + 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; } } From 1ed8361f8dc6e8e902bbaf96f38f83704a0e8be3 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 12 Feb 2026 23:15:13 +0530 Subject: [PATCH 08/10] Added all test cases --- Lib/test/test_ctypes/test_arrays.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index be044379a3845f..9732db27eca324 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) @@ -191,6 +191,7 @@ class S2(Structure): 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() @@ -198,9 +199,8 @@ def test_ctypes_array_class_assignment_pointer_arrays(self): def test_ctypes_array_from_param_incompatible(self): A = c_int * 3 - B = c_double * 3 with self.assertRaises(TypeError): - A.from_param(B()) + A.from_param(object()) def test_step_overflow(self): a = (c_int * 5)() From d78420338fd4fcb48ecd5dc2f82ff9b6b15b27fe Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 13 Feb 2026 23:01:16 +0530 Subject: [PATCH 09/10] Error Fixed --- Lib/test/test_ctypes/test_arrays.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index 9732db27eca324..fade7496a97c1b 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -183,10 +183,8 @@ def test_ctypes_array_class_assignment_same_size_ints(self): def test_ctypes_array_class_assignment_structs(self): class S1(Structure): _fields_ = [("x", c_int)] - class S2(Structure): - _fields_ = [("x", c_int)] A = S1 * 2 - B = S2 * 2 + B = S1 * 2 a = A() a.__class__ = B From ab6788453ea489cef59e67702b96fc7f75ed8de2 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 13 Feb 2026 23:04:21 +0530 Subject: [PATCH 10/10] Lint Fixed --- 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 fade7496a97c1b..b76e76a22c6c48 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -184,7 +184,7 @@ def test_ctypes_array_class_assignment_structs(self): class S1(Structure): _fields_ = [("x", c_int)] A = S1 * 2 - B = S1 * 2 + B = S1 * 2 a = A() a.__class__ = B