From 718cb8bec0b308205029618ccaeef81519fe8ef2 Mon Sep 17 00:00:00 2001 From: Saugat Pandey <39925335+iamsaugatpandey@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:21:06 -0500 Subject: [PATCH] [3.15] gh-160708: Use the type size for resized ctypes simple objects (GH-160720) After ctypes.resize(), a simple object's buffer is larger than its type. bool() and passing the object to a foreign function still used the buffer size, so bool() read past a 16-byte string literal and the call copied the whole buffer into the fixed-size PyCArgObject value, corrupting the heap. Use the type's size instead, like the value getter does since gh-159739. bool() now checks the value's bytes directly, which also covers simple types larger than 16 bytes such as c_longdouble_complex on x86-64. (cherry picked from commit 44579b0da558b5a45b17358dffbc4a2ee947cdf7) Co-authored-by: Saugat Pandey <39925335+iamsaugatpandey@users.noreply.github.com> Co-authored-by: Peter Bierma --- Lib/test/test_ctypes/test_cfuncs.py | 10 ++++++++ Lib/test/test_ctypes/test_numbers.py | 23 +++++++++++++++++++ ...-10-26-16-17-45.gh-issue-160708.w7z33g.rst | 3 +++ Modules/_ctypes/_ctypes.c | 21 +++++++++++++---- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-26-16-17-45.gh-issue-160708.w7z33g.rst diff --git a/Lib/test/test_ctypes/test_cfuncs.py b/Lib/test/test_ctypes/test_cfuncs.py index 937be8eaa95c19d..c87bb7b7a40ba8e 100644 --- a/Lib/test/test_ctypes/test_cfuncs.py +++ b/Lib/test/test_ctypes/test_cfuncs.py @@ -73,6 +73,16 @@ def test_int(self): self.assertEqual(self._dll.tf_i(-2147483646), -715827882) self.assertEqual(self.S(), -2147483646) + def test_int_resized(self): + # gh-160708: only the value of a resized object is passed, + # not the extra space added by resize(). + self._dll.tf_i.restype = c_int + self._dll.tf_i.argtypes = (c_int,) + value = c_int(-2147483646) + ctypes.resize(value, 1024) + self.assertEqual(self._dll.tf_i(value), -715827882) + self.assertEqual(self.S(), -2147483646) + def test_int_plus(self): self._dll.tf_bi.restype = c_int self._dll.tf_bi.argtypes = (c_byte, c_int) diff --git a/Lib/test/test_ctypes/test_numbers.py b/Lib/test/test_ctypes/test_numbers.py index c497c09cf2b7271..90b1e81dbe47a23 100644 --- a/Lib/test/test_ctypes/test_numbers.py +++ b/Lib/test/test_ctypes/test_numbers.py @@ -74,6 +74,19 @@ def test_resized_scalar_value(self): ctypes.resize(value, 65536) self.assertEqual(value.value, 17) + def test_resized_scalar_bool(self): + # gh-160708: bool() only looks at the value, not at the extra + # space added by resize(). + for size in (16, 1024): + with self.subTest(size=size): + value = c_int(0) + ctypes.resize(value, size) + ctypes.memset(ctypes.addressof(value) + sizeof(c_int), 1, + size - sizeof(c_int)) + self.assertIs(bool(value), False) + value.value = 1 + self.assertIs(bool(value), True) + def test_unsigned_values(self): # the value given to the constructor is available # as the 'value' attribute @@ -142,6 +155,16 @@ def test_complex(self): array = (t * 3)() self.assertEqual(memoryview(array).format, prefix + format) + @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), + "requires C11 complex type") + def test_complex_bool(self): + # gh-160708: long double complex can be larger than 16 bytes. + for t in [ctypes.c_double_complex, ctypes.c_float_complex, + ctypes.c_longdouble_complex]: + with self.subTest(t=t): + self.assertIs(bool(t()), False) + self.assertIs(bool(t(1j)), True) + @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") def test_complex_round_trip(self): diff --git a/Misc/NEWS.d/next/Library/2026-10-26-16-17-45.gh-issue-160708.w7z33g.rst b/Misc/NEWS.d/next/Library/2026-10-26-16-17-45.gh-issue-160708.w7z33g.rst new file mode 100644 index 000000000000000..7a88edad93f7c27 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-26-16-17-45.gh-issue-160708.w7z33g.rst @@ -0,0 +1,3 @@ +Fix out-of-bounds memory access in :func:`bool` on :mod:`ctypes` simple +objects enlarged by :func:`ctypes.resize`, and when passing such objects to +a foreign function. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index e50b1257d343405..7d486e596f2d92b 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -2335,7 +2335,7 @@ PyCSimpleType_paramfunc(ctypes_state *st, CDataObject *self) parg->tag = fd->code; parg->pffi_type = fd->pffi_type; parg->obj = Py_NewRef(self); - memcpy(&parg->value, self->b_ptr, self->b_size); + memcpy(&parg->value, self->b_ptr, info->size); return parg; } @@ -5522,12 +5522,25 @@ static PyMethodDef Simple_methods[] = { static int Simple_bool(PyObject *op) { - int cmp; CDataObject *self = _CDataObject_CAST(op); + ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); + StgInfo *info; + if (PyStgInfo_FromObject(st, op, &info) < 0) { + return -1; + } + assert(info != NULL); + + int res = 0; Py_BEGIN_CRITICAL_SECTION(self); - cmp = memcmp(self->b_ptr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", self->b_size); + assert(info->size <= self->b_size); + for (Py_ssize_t i = 0; i < info->size; i++) { + if (self->b_ptr[i]) { + res = 1; + break; + } + } Py_END_CRITICAL_SECTION(); - return cmp; + return res; } /* "%s(%s)" % (self.__class__.__name__, self.value) */