Skip to content

Commit 129c2b3

Browse files
authored
bpo-38368: Added fix for ctypes crash when handling arrays in structs/unions. (GH-16589) (GH-16672)
(cherry picked from commit e8bedbd)
1 parent 443370d commit 129c2b3

File tree

3 files changed

+231
-25
lines changed

3 files changed

+231
-25
lines changed

Lib/ctypes/test/test_structures.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import platform
2+
import sys
13
import unittest
24
from ctypes import *
35
from ctypes.test import need_symbol
@@ -452,6 +454,16 @@ class Test3(Structure):
452454
('data', c_double * 2),
453455
]
454456

457+
class Test3A(Structure):
458+
_fields_ = [
459+
('data', c_float * 2),
460+
]
461+
462+
class Test3B(Test3A):
463+
_fields_ = [
464+
('more_data', c_float * 2),
465+
]
466+
455467
s = Test2()
456468
expected = 0
457469
for i in range(16):
@@ -480,6 +492,46 @@ class Test3(Structure):
480492
self.assertEqual(s.data[0], 3.14159)
481493
self.assertEqual(s.data[1], 2.71828)
482494

495+
s = Test3B()
496+
s.data[0] = 3.14159
497+
s.data[1] = 2.71828
498+
s.more_data[0] = -3.0
499+
s.more_data[1] = -2.0
500+
501+
expected = 3.14159 + 2.71828 - 5.0
502+
func = dll._testfunc_array_in_struct2a
503+
func.restype = c_double
504+
func.argtypes = (Test3B,)
505+
result = func(s)
506+
self.assertAlmostEqual(result, expected, places=6)
507+
# check the passed-in struct hasn't changed
508+
self.assertAlmostEqual(s.data[0], 3.14159, places=6)
509+
self.assertAlmostEqual(s.data[1], 2.71828, places=6)
510+
self.assertAlmostEqual(s.more_data[0], -3.0, places=6)
511+
self.assertAlmostEqual(s.more_data[1], -2.0, places=6)
512+
513+
def test_38368(self):
514+
class U(Union):
515+
_fields_ = [
516+
('f1', c_uint8 * 16),
517+
('f2', c_uint16 * 8),
518+
('f3', c_uint32 * 4),
519+
]
520+
u = U()
521+
u.f3[0] = 0x01234567
522+
u.f3[1] = 0x89ABCDEF
523+
u.f3[2] = 0x76543210
524+
u.f3[3] = 0xFEDCBA98
525+
f1 = [u.f1[i] for i in range(16)]
526+
f2 = [u.f2[i] for i in range(8)]
527+
if sys.byteorder == 'little':
528+
self.assertEqual(f1, [0x67, 0x45, 0x23, 0x01,
529+
0xef, 0xcd, 0xab, 0x89,
530+
0x10, 0x32, 0x54, 0x76,
531+
0x98, 0xba, 0xdc, 0xfe])
532+
self.assertEqual(f2, [0x4567, 0x0123, 0xcdef, 0x89ab,
533+
0x3210, 0x7654, 0xba98, 0xfedc])
534+
483535
class PointerMemberTestCase(unittest.TestCase):
484536

485537
def test(self):

Modules/_ctypes/_ctypes_test.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ typedef struct {
100100
double data[2];
101101
} Test3;
102102

103+
typedef struct {
104+
float data[2];
105+
float more_data[2];
106+
} Test3B;
107+
103108
EXPORT(double)
104109
_testfunc_array_in_struct2(Test3 in)
105110
{
@@ -114,6 +119,22 @@ _testfunc_array_in_struct2(Test3 in)
114119
return result;
115120
}
116121

122+
EXPORT(double)
123+
_testfunc_array_in_struct2a(Test3B in)
124+
{
125+
double result = 0;
126+
127+
for (unsigned i = 0; i < 2; i++)
128+
result += in.data[i];
129+
for (unsigned i = 0; i < 2; i++)
130+
result += in.more_data[i];
131+
/* As the structure is passed by value, changes to it shouldn't be
132+
* reflected in the caller.
133+
*/
134+
memset(in.data, 0, sizeof(in.data));
135+
return result;
136+
}
137+
117138
EXPORT(void)testfunc_array(int values[4])
118139
{
119140
printf("testfunc_array %d %d %d %d\n",

Modules/_ctypes/stgdict.c

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -611,9 +611,9 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
611611
stgdict->align = total_align;
612612
stgdict->length = len; /* ADD ffi_ofs? */
613613

614-
#define MAX_ELEMENTS 16
614+
#define MAX_STRUCT_SIZE 16
615615

616-
if (arrays_seen && (size <= 16)) {
616+
if (arrays_seen && (size <= MAX_STRUCT_SIZE)) {
617617
/*
618618
* See bpo-22273. Arrays are normally treated as pointers, which is
619619
* fine when an array name is being passed as parameter, but not when
@@ -634,11 +634,49 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
634634
* Although the passing in registers is specific to 64-bit Linux, the
635635
* array-in-struct vs. pointer problem is general. But we restrict the
636636
* type transformation to small structs nonetheless.
637+
*
638+
* Note that although a union may be small in terms of memory usage, it
639+
* could contain many overlapping declarations of arrays, e.g.
640+
*
641+
* union {
642+
* unsigned int_8 foo [16];
643+
* unsigned uint_8 bar [16];
644+
* unsigned int_16 baz[8];
645+
* unsigned uint_16 bozz[8];
646+
* unsigned int_32 fizz[4];
647+
* unsigned uint_32 buzz[4];
648+
* }
649+
*
650+
* which is still only 16 bytes in size. We need to convert this into
651+
* the following equivalent for libffi:
652+
*
653+
* union {
654+
* struct { int_8 e1; int_8 e2; ... int_8 e_16; } f1;
655+
* struct { uint_8 e1; uint_8 e2; ... uint_8 e_16; } f2;
656+
* struct { int_16 e1; int_16 e2; ... int_16 e_8; } f3;
657+
* struct { uint_16 e1; uint_16 e2; ... uint_16 e_8; } f4;
658+
* struct { int_32 e1; int_32 e2; ... int_32 e_4; } f5;
659+
* struct { uint_32 e1; uint_32 e2; ... uint_32 e_4; } f6;
660+
* }
661+
*
662+
* So the struct/union needs setting up as follows: all non-array
663+
* elements copied across as is, and all array elements replaced with
664+
* an equivalent struct which has as many fields as the array has
665+
* elements, plus one NULL pointer.
637666
*/
638-
ffi_type *actual_types[MAX_ELEMENTS + 1];
639-
int actual_type_index = 0;
640667

641-
memset(actual_types, 0, sizeof(actual_types));
668+
Py_ssize_t num_ffi_type_pointers = 0; /* for the dummy fields */
669+
Py_ssize_t num_ffi_types = 0; /* for the dummy structures */
670+
size_t alloc_size; /* total bytes to allocate */
671+
void *type_block; /* to hold all the type information needed */
672+
ffi_type **element_types; /* of this struct/union */
673+
ffi_type **dummy_types; /* of the dummy struct elements */
674+
ffi_type *structs; /* point to struct aliases of arrays */
675+
Py_ssize_t element_index; /* index into element_types for this */
676+
Py_ssize_t dummy_index = 0; /* index into dummy field pointers */
677+
Py_ssize_t struct_index = 0; /* index into dummy structs */
678+
679+
/* first pass to see how much memory to allocate */
642680
for (i = 0; i < len; ++i) {
643681
PyObject *name, *desc;
644682
PyObject *pair = PySequence_GetItem(fields, i);
@@ -648,24 +686,123 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
648686
if (pair == NULL) {
649687
return -1;
650688
}
689+
if (!PyArg_ParseTuple(pair, "UO|i", &name, &desc, &bitsize)) {
690+
PyErr_SetString(PyExc_TypeError,
691+
"'_fields_' must be a sequence of (name, C type) pairs");
692+
Py_DECREF(pair);
693+
return -1;
694+
}
695+
dict = PyType_stgdict(desc);
696+
if (dict == NULL) {
697+
Py_DECREF(pair);
698+
PyErr_Format(PyExc_TypeError,
699+
"second item in _fields_ tuple (index %zd) must be a C type",
700+
i);
701+
return -1;
702+
}
703+
if (!PyCArrayTypeObject_Check(desc)) {
704+
/* Not an array. Just need an ffi_type pointer. */
705+
num_ffi_type_pointers++;
706+
}
707+
else {
708+
/* It's an array. */
709+
Py_ssize_t length = dict->length;
710+
StgDictObject *edict;
711+
712+
edict = PyType_stgdict(dict->proto);
713+
if (edict == NULL) {
714+
Py_DECREF(pair);
715+
PyErr_Format(PyExc_TypeError,
716+
"second item in _fields_ tuple (index %zd) must be a C type",
717+
i);
718+
return -1;
719+
}
720+
/*
721+
* We need one extra ffi_type to hold the struct, and one
722+
* ffi_type pointer per array element + one for a NULL to
723+
* mark the end.
724+
*/
725+
num_ffi_types++;
726+
num_ffi_type_pointers += length + 1;
727+
}
728+
Py_DECREF(pair);
729+
}
730+
731+
/*
732+
* At this point, we know we need storage for some ffi_types and some
733+
* ffi_type pointers. We'll allocate these in one block.
734+
* There are three sub-blocks of information: the ffi_type pointers to
735+
* this structure/union's elements, the ffi_type_pointers to the
736+
* dummy fields standing in for array elements, and the
737+
* ffi_types representing the dummy structures.
738+
*/
739+
alloc_size = (ffi_ofs + 1 + len + num_ffi_type_pointers) * sizeof(ffi_type *) +
740+
num_ffi_types * sizeof(ffi_type);
741+
type_block = PyMem_Malloc(alloc_size);
742+
743+
if (type_block == NULL) {
744+
PyErr_NoMemory();
745+
return -1;
746+
}
747+
/*
748+
* the first block takes up ffi_ofs + len + 1 which is the pointers *
749+
* for this struct/union. The second block takes up
750+
* num_ffi_type_pointers, so the sum of these is ffi_ofs + len + 1 +
751+
* num_ffi_type_pointers as allocated above. The last bit is the
752+
* num_ffi_types structs.
753+
*/
754+
element_types = (ffi_type **) type_block;
755+
dummy_types = &element_types[ffi_ofs + len + 1];
756+
structs = (ffi_type *) &dummy_types[num_ffi_type_pointers];
757+
758+
if (num_ffi_types > 0) {
759+
memset(structs, 0, num_ffi_types * sizeof(ffi_type));
760+
}
761+
if (ffi_ofs && (basedict != NULL)) {
762+
memcpy(element_types,
763+
basedict->ffi_type_pointer.elements,
764+
ffi_ofs * sizeof(ffi_type *));
765+
}
766+
element_index = ffi_ofs;
767+
768+
/* second pass to actually set the type pointers */
769+
for (i = 0; i < len; ++i) {
770+
PyObject *name, *desc;
771+
PyObject *pair = PySequence_GetItem(fields, i);
772+
StgDictObject *dict;
773+
int bitsize = 0;
774+
775+
if (pair == NULL) {
776+
PyMem_Free(type_block);
777+
return -1;
778+
}
779+
/* In theory, we made this call in the first pass, so it *shouldn't*
780+
* fail. However, you never know, and the code above might change
781+
* later - keeping the check in here is a tad defensive but it
782+
* will affect program size only slightly and performance hardly at
783+
* all.
784+
*/
651785
if (!PyArg_ParseTuple(pair, "UO|i", &name, &desc, &bitsize)) {
652786
PyErr_SetString(PyExc_TypeError,
653787
"'_fields_' must be a sequence of (name, C type) pairs");
654-
Py_XDECREF(pair);
788+
Py_DECREF(pair);
789+
PyMem_Free(type_block);
655790
return -1;
656791
}
657792
dict = PyType_stgdict(desc);
793+
/* Possibly this check could be avoided, but see above comment. */
658794
if (dict == NULL) {
659795
Py_DECREF(pair);
796+
PyMem_Free(type_block);
660797
PyErr_Format(PyExc_TypeError,
661798
"second item in _fields_ tuple (index %zd) must be a C type",
662799
i);
663800
return -1;
664801
}
802+
assert(element_index < (ffi_ofs + len)); /* will be used below */
665803
if (!PyCArrayTypeObject_Check(desc)) {
666804
/* Not an array. Just copy over the element ffi_type. */
667-
actual_types[actual_type_index++] = &dict->ffi_type_pointer;
668-
assert(actual_type_index <= MAX_ELEMENTS);
805+
element_types[element_index++] = &dict->ffi_type_pointer;
669806
}
670807
else {
671808
int length = dict->length;
@@ -674,42 +811,38 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
674811
edict = PyType_stgdict(dict->proto);
675812
if (edict == NULL) {
676813
Py_DECREF(pair);
814+
PyMem_Free(type_block);
677815
PyErr_Format(PyExc_TypeError,
678816
"second item in _fields_ tuple (index %zd) must be a C type",
679817
i);
680818
return -1;
681819
}
820+
element_types[element_index++] = &structs[struct_index];
821+
structs[struct_index].size = length * edict->ffi_type_pointer.size;
822+
structs[struct_index].alignment = edict->ffi_type_pointer.alignment;
823+
structs[struct_index].type = FFI_TYPE_STRUCT;
824+
structs[struct_index].elements = &dummy_types[dummy_index];
825+
++struct_index;
682826
/* Copy over the element's type, length times. */
683827
while (length > 0) {
684-
actual_types[actual_type_index++] = &edict->ffi_type_pointer;
685-
assert(actual_type_index <= MAX_ELEMENTS);
828+
assert(dummy_index < (num_ffi_type_pointers));
829+
dummy_types[dummy_index++] = &edict->ffi_type_pointer;
686830
length--;
687831
}
832+
assert(dummy_index < (num_ffi_type_pointers));
833+
dummy_types[dummy_index++] = NULL;
688834
}
689835
Py_DECREF(pair);
690836
}
691837

692-
actual_types[actual_type_index++] = NULL;
838+
element_types[element_index] = NULL;
693839
/*
694840
* Replace the old elements with the new, taking into account
695841
* base class elements where necessary.
696842
*/
697843
assert(stgdict->ffi_type_pointer.elements);
698844
PyMem_Free(stgdict->ffi_type_pointer.elements);
699-
stgdict->ffi_type_pointer.elements = PyMem_New(ffi_type *,
700-
ffi_ofs + actual_type_index);
701-
if (stgdict->ffi_type_pointer.elements == NULL) {
702-
PyErr_NoMemory();
703-
return -1;
704-
}
705-
if (ffi_ofs) {
706-
memcpy(stgdict->ffi_type_pointer.elements,
707-
basedict->ffi_type_pointer.elements,
708-
ffi_ofs * sizeof(ffi_type *));
709-
710-
}
711-
memcpy(&stgdict->ffi_type_pointer.elements[ffi_ofs], actual_types,
712-
actual_type_index * sizeof(ffi_type *));
845+
stgdict->ffi_type_pointer.elements = element_types;
713846
}
714847

715848
/* We did check that this flag was NOT set above, it must not

0 commit comments

Comments
 (0)