Skip to content

Commit 6d9af66

Browse files
[3.12] gh-62260: Fix ctypes.Structure subclassing with multiple layers (GH-13374) (GH-113623)
The length field of StgDictObject for Structure class contains now the total number of items in ffi_type_pointer.elements (excluding the trailing null). The old behavior of using the number of elements in the parent class can cause the array to be truncated when it is copied, especially when there are multiple layers of subclassing. (cherry picked from commit 5f3cc90) Co-authored-by: Jeffrey Kintscher <[email protected]>
1 parent e132751 commit 6d9af66

File tree

4 files changed

+71
-7
lines changed

4 files changed

+71
-7
lines changed

Lib/test/test_ctypes/test_structures.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import platform
2+
from platform import architecture as _architecture
3+
import struct
24
import sys
35
import unittest
46
from test.test_ctypes import need_symbol
@@ -7,6 +9,7 @@
79
c_uint8, c_uint16, c_uint32,
810
c_short, c_ushort, c_int, c_uint,
911
c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double)
12+
from ctypes.util import find_library
1013
from struct import calcsize
1114
import _ctypes_test
1215
from collections import namedtuple
@@ -189,7 +192,6 @@ class X(Structure):
189192
self.assertEqual(sizeof(X), 10)
190193
self.assertEqual(X.b.offset, 2)
191194

192-
import struct
193195
longlong_size = struct.calcsize("q")
194196
longlong_align = struct.calcsize("bq") - longlong_size
195197

@@ -480,6 +482,66 @@ class X(Structure):
480482
self.assertEqual(s.first, got.first)
481483
self.assertEqual(s.second, got.second)
482484

485+
def _test_issue18060(self, Vector):
486+
# The call to atan2() should succeed if the
487+
# class fields were correctly cloned in the
488+
# subclasses. Otherwise, it will segfault.
489+
if sys.platform == 'win32':
490+
libm = CDLL(find_library('msvcrt.dll'))
491+
else:
492+
libm = CDLL(find_library('m'))
493+
494+
libm.atan2.argtypes = [Vector]
495+
libm.atan2.restype = c_double
496+
497+
arg = Vector(y=0.0, x=-1.0)
498+
self.assertAlmostEqual(libm.atan2(arg), 3.141592653589793)
499+
500+
@unittest.skipIf(_architecture() == ('64bit', 'WindowsPE'), "can't test Windows x64 build")
501+
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
502+
def test_issue18060_a(self):
503+
# This test case calls
504+
# PyCStructUnionType_update_stgdict() for each
505+
# _fields_ assignment, and PyCStgDict_clone()
506+
# for the Mid and Vector class definitions.
507+
class Base(Structure):
508+
_fields_ = [('y', c_double),
509+
('x', c_double)]
510+
class Mid(Base):
511+
pass
512+
Mid._fields_ = []
513+
class Vector(Mid): pass
514+
self._test_issue18060(Vector)
515+
516+
@unittest.skipIf(_architecture() == ('64bit', 'WindowsPE'), "can't test Windows x64 build")
517+
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
518+
def test_issue18060_b(self):
519+
# This test case calls
520+
# PyCStructUnionType_update_stgdict() for each
521+
# _fields_ assignment.
522+
class Base(Structure):
523+
_fields_ = [('y', c_double),
524+
('x', c_double)]
525+
class Mid(Base):
526+
_fields_ = []
527+
class Vector(Mid):
528+
_fields_ = []
529+
self._test_issue18060(Vector)
530+
531+
@unittest.skipIf(_architecture() == ('64bit', 'WindowsPE'), "can't test Windows x64 build")
532+
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
533+
def test_issue18060_c(self):
534+
# This test case calls
535+
# PyCStructUnionType_update_stgdict() for each
536+
# _fields_ assignment.
537+
class Base(Structure):
538+
_fields_ = [('y', c_double)]
539+
class Mid(Base):
540+
_fields_ = []
541+
class Vector(Mid):
542+
_fields_ = [('x', c_double)]
543+
self._test_issue18060(Vector)
544+
483545
def test_array_in_struct(self):
484546
# See bpo-22273
485547

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a class inheritance issue that can cause segfaults when deriving two or more levels of subclasses from a base class of Structure or Union.
2+

Modules/_ctypes/_ctypes.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4352,10 +4352,10 @@ _init_pos_args(PyObject *self, PyTypeObject *type,
43524352
return index;
43534353
}
43544354

4355-
for (i = 0;
4356-
i < dict->length && (i+index) < PyTuple_GET_SIZE(args);
4355+
for (i = index;
4356+
i < dict->length && i < PyTuple_GET_SIZE(args);
43574357
++i) {
4358-
PyObject *pair = PySequence_GetItem(fields, i);
4358+
PyObject *pair = PySequence_GetItem(fields, i - index);
43594359
PyObject *name, *val;
43604360
int res;
43614361
if (!pair)
@@ -4365,7 +4365,7 @@ _init_pos_args(PyObject *self, PyTypeObject *type,
43654365
Py_DECREF(pair);
43664366
return -1;
43674367
}
4368-
val = PyTuple_GET_ITEM(args, i + index);
4368+
val = PyTuple_GET_ITEM(args, i);
43694369
if (kwds) {
43704370
res = PyDict_Contains(kwds, name);
43714371
if (res != 0) {
@@ -4386,7 +4386,7 @@ _init_pos_args(PyObject *self, PyTypeObject *type,
43864386
if (res == -1)
43874387
return -1;
43884388
}
4389-
return index + dict->length;
4389+
return dict->length;
43904390
}
43914391

43924392
static int

Modules/_ctypes/stgdict.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
694694

695695
stgdict->size = aligned_size;
696696
stgdict->align = total_align;
697-
stgdict->length = len; /* ADD ffi_ofs? */
697+
stgdict->length = ffi_ofs + len;
698698

699699
/*
700700
* The value of MAX_STRUCT_SIZE depends on the platform Python is running on.

0 commit comments

Comments
 (0)