e702ad4
From 091ae930cd8bac805a083ea68e4f886200f57de0 Mon Sep 17 00:00:00 2001
e702ad4
From: "Miss Islington (bot)"
e702ad4
 <31488909+miss-islington@users.noreply.github.com>
e702ad4
Date: Fri, 30 Aug 2019 05:50:44 -0700
e702ad4
Subject: [PATCH] 00331: Fix StructUnionType_paramfunc()
e702ad4
e702ad4
Fix a ctypes regression of Python 3.8. When a ctypes.Structure is
e702ad4
passed by copy to a function, ctypes internals created a temporary
e702ad4
object which had the side effect of calling the structure finalizer
e702ad4
(__del__) twice. The Python semantics requires a finalizer to be
e702ad4
called exactly once. Fix ctypes internals to no longer call the
e702ad4
finalizer twice.
e702ad4
e702ad4
Create a new internal StructParam_Type which is only used by
e702ad4
_ctypes_callproc() to call PyMem_Free(ptr) on Py_DECREF(argument).
e702ad4
StructUnionType_paramfunc() creates such object.
e702ad4
(cherry picked from commit 96b4087ce784ee7434dffdf69c475f5b40543982)
e702ad4
e702ad4
Co-authored-by: Victor Stinner <vstinner@redhat.com>
e702ad4
---
e702ad4
 Lib/ctypes/test/test_structures.py            | 51 +++++++++++--
e702ad4
 .../2019-08-30-11-21-10.bpo-37140.cFAX-a.rst  |  5 ++
e702ad4
 Modules/_ctypes/_ctypes.c                     | 73 +++++++++++++++----
e702ad4
 3 files changed, 109 insertions(+), 20 deletions(-)
e702ad4
 create mode 100644 Misc/NEWS.d/next/Library/2019-08-30-11-21-10.bpo-37140.cFAX-a.rst
e702ad4
e702ad4
diff --git a/Lib/ctypes/test/test_structures.py b/Lib/ctypes/test/test_structures.py
e702ad4
index d1ea43bc7e..fda104563d 100644
e702ad4
--- a/Lib/ctypes/test/test_structures.py
e702ad4
+++ b/Lib/ctypes/test/test_structures.py
e702ad4
@@ -3,7 +3,7 @@ from ctypes import *
e702ad4
 from ctypes.test import need_symbol
e702ad4
 from struct import calcsize
e702ad4
 import _ctypes_test
e702ad4
-import test.support
e702ad4
+from test import support
e702ad4
 
e702ad4
 class SubclassesTest(unittest.TestCase):
e702ad4
     def test_subclass(self):
e702ad4
@@ -202,7 +202,7 @@ class StructureTestCase(unittest.TestCase):
e702ad4
              "_pack_": -1}
e702ad4
         self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
e702ad4
 
e702ad4
-    @test.support.cpython_only
e702ad4
+    @support.cpython_only
e702ad4
     def test_packed_c_limits(self):
e702ad4
         # Issue 15989
e702ad4
         import _testcapi
e702ad4
@@ -396,27 +396,66 @@ class StructureTestCase(unittest.TestCase):
e702ad4
         self.assertRaises(TypeError, lambda: Z(1, 2, 3, 4, 5, 6, 7))
e702ad4
 
e702ad4
     def test_pass_by_value(self):
e702ad4
-        # This should mirror the structure in Modules/_ctypes/_ctypes_test.c
e702ad4
-        class X(Structure):
e702ad4
+        # This should mirror the Test structure
e702ad4
+        # in Modules/_ctypes/_ctypes_test.c
e702ad4
+        class Test(Structure):
e702ad4
             _fields_ = [
e702ad4
                 ('first', c_ulong),
e702ad4
                 ('second', c_ulong),
e702ad4
                 ('third', c_ulong),
e702ad4
             ]
e702ad4
 
e702ad4
-        s = X()
e702ad4
+        s = Test()
e702ad4
         s.first = 0xdeadbeef
e702ad4
         s.second = 0xcafebabe
e702ad4
         s.third = 0x0bad1dea
e702ad4
         dll = CDLL(_ctypes_test.__file__)
e702ad4
         func = dll._testfunc_large_struct_update_value
e702ad4
-        func.argtypes = (X,)
e702ad4
+        func.argtypes = (Test,)
e702ad4
         func.restype = None
e702ad4
         func(s)
e702ad4
         self.assertEqual(s.first, 0xdeadbeef)
e702ad4
         self.assertEqual(s.second, 0xcafebabe)
e702ad4
         self.assertEqual(s.third, 0x0bad1dea)
e702ad4
 
e702ad4
+    def test_pass_by_value_finalizer(self):
e702ad4
+        # bpo-37140: Similar to test_pass_by_value(), but the Python structure
e702ad4
+        # has a finalizer (__del__() method): the finalizer must only be called
e702ad4
+        # once.
e702ad4
+
e702ad4
+        finalizer_calls = []
e702ad4
+
e702ad4
+        class Test(Structure):
e702ad4
+            _fields_ = [
e702ad4
+                ('first', c_ulong),
e702ad4
+                ('second', c_ulong),
e702ad4
+                ('third', c_ulong),
e702ad4
+            ]
e702ad4
+            def __del__(self):
e702ad4
+                finalizer_calls.append("called")
e702ad4
+
e702ad4
+        s = Test(1, 2, 3)
e702ad4
+        # Test the StructUnionType_paramfunc() code path which copies the
e702ad4
+        # structure: if the stucture is larger than sizeof(void*).
e702ad4
+        self.assertGreater(sizeof(s), sizeof(c_void_p))
e702ad4
+
e702ad4
+        dll = CDLL(_ctypes_test.__file__)
e702ad4
+        func = dll._testfunc_large_struct_update_value
e702ad4
+        func.argtypes = (Test,)
e702ad4
+        func.restype = None
e702ad4
+        func(s)
e702ad4
+        # bpo-37140: Passing the structure by refrence must not call
e702ad4
+        # its finalizer!
e702ad4
+        self.assertEqual(finalizer_calls, [])
e702ad4
+        self.assertEqual(s.first, 1)
e702ad4
+        self.assertEqual(s.second, 2)
e702ad4
+        self.assertEqual(s.third, 3)
e702ad4
+
e702ad4
+        # The finalizer must be called exactly once
e702ad4
+        s = None
e702ad4
+        support.gc_collect()
e702ad4
+        self.assertEqual(finalizer_calls, ["called"])
e702ad4
+
e702ad4
     def test_pass_by_value_in_register(self):
e702ad4
         class X(Structure):
e702ad4
             _fields_ = [
e702ad4
diff --git a/Misc/NEWS.d/next/Library/2019-08-30-11-21-10.bpo-37140.cFAX-a.rst b/Misc/NEWS.d/next/Library/2019-08-30-11-21-10.bpo-37140.cFAX-a.rst
e702ad4
new file mode 100644
e702ad4
index 0000000000..4eaa226147
e702ad4
--- /dev/null
e702ad4
+++ b/Misc/NEWS.d/next/Library/2019-08-30-11-21-10.bpo-37140.cFAX-a.rst
e702ad4
@@ -0,0 +1,5 @@
e702ad4
+Fix a ctypes regression of Python 3.8. When a ctypes.Structure is passed by
e702ad4
+copy to a function, ctypes internals created a temporary object which had
e702ad4
+the side effect of calling the structure finalizer (__del__) twice. The
e702ad4
+Python semantics requires a finalizer to be called exactly once. Fix ctypes
e702ad4
+internals to no longer call the finalizer twice.
e702ad4
diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c
e702ad4
index 2201c4520a..4728874006 100644
e702ad4
--- a/Modules/_ctypes/_ctypes.c
e702ad4
+++ b/Modules/_ctypes/_ctypes.c
e702ad4
@@ -392,6 +392,35 @@ _ctypes_alloc_format_string_with_shape(int ndim, const Py_ssize_t *shape,
e702ad4
     return result;
e702ad4
 }
e702ad4
 
e702ad4
+/* StructParamObject and StructParam_Type are used in _ctypes_callproc()
e702ad4
+   for argument.keep to call PyMem_Free(ptr) on Py_DECREF(argument).
e702ad4
+
e702ad4
+   StructUnionType_paramfunc() creates such object when a ctypes Structure is
e702ad4
+   passed by copy to a C function. */
e702ad4
+typedef struct {
e702ad4
+    PyObject_HEAD
e702ad4
+    void *ptr;
e702ad4
+} StructParamObject;
e702ad4
+
e702ad4
+
e702ad4
+static void
e702ad4
+StructParam_dealloc(PyObject *myself)
e702ad4
+{
e702ad4
+    StructParamObject *self = (StructParamObject *)myself;
e702ad4
+    PyMem_Free(self->ptr);
e702ad4
+    Py_TYPE(self)->tp_free(myself);
e702ad4
+}
e702ad4
+
e702ad4
+
e702ad4
+static PyTypeObject StructParam_Type = {
e702ad4
+    PyVarObject_HEAD_INIT(NULL, 0)
e702ad4
+    .tp_name = "_ctypes.StructParam_Type",
e702ad4
+    .tp_basicsize = sizeof(StructParamObject),
e702ad4
+    .tp_dealloc = StructParam_dealloc,
e702ad4
+    .tp_flags = Py_TPFLAGS_DEFAULT,
e702ad4
+};
e702ad4
+
e702ad4
+
e702ad4
 /*
e702ad4
   PyCStructType_Type - a meta type/class.  Creating a new class using this one as
e702ad4
   __metaclass__ will call the constructor StructUnionType_new.  It replaces the
e702ad4
@@ -403,35 +432,47 @@ static PyCArgObject *
e702ad4
 StructUnionType_paramfunc(CDataObject *self)
e702ad4
 {
e702ad4
     PyCArgObject *parg;
e702ad4
-    CDataObject *copied_self;
e702ad4
+    PyObject *obj;
e702ad4
     StgDictObject *stgdict;
e702ad4
+    void *ptr;
e702ad4
 
e702ad4
     if ((size_t)self->b_size > sizeof(void*)) {
e702ad4
-        void *new_ptr = PyMem_Malloc(self->b_size);
e702ad4
-        if (new_ptr == NULL)
e702ad4
+        ptr = PyMem_Malloc(self->b_size);
e702ad4
+        if (ptr == NULL) {
e702ad4
             return NULL;
e702ad4
-        memcpy(new_ptr, self->b_ptr, self->b_size);
e702ad4
-        copied_self = (CDataObject *)PyCData_AtAddress(
e702ad4
-            (PyObject *)Py_TYPE(self), new_ptr);
e702ad4
-        copied_self->b_needsfree = 1;
e702ad4
+        }
e702ad4
+        memcpy(ptr, self->b_ptr, self->b_size);
e702ad4
+
e702ad4
+        /* Create a Python object which calls PyMem_Free(ptr) in
e702ad4
+           its deallocator. The object will be destroyed
e702ad4
+           at _ctypes_callproc() cleanup. */
e702ad4
+        obj = (&StructParam_Type)->tp_alloc(&StructParam_Type, 0);
e702ad4
+        if (obj == NULL) {
e702ad4
+            PyMem_Free(ptr);
e702ad4
+            return NULL;
e702ad4
+        }
e702ad4
+
e702ad4
+        StructParamObject *struct_param = (StructParamObject *)obj;
e702ad4
+        struct_param->ptr = ptr;
e702ad4
     } else {
e702ad4
-        copied_self = self;
e702ad4
-        Py_INCREF(copied_self);
e702ad4
+        ptr = self->b_ptr;
e702ad4
+        obj = (PyObject *)self;
e702ad4
+        Py_INCREF(obj);
e702ad4
     }
e702ad4
 
e702ad4
     parg = PyCArgObject_new();
e702ad4
     if (parg == NULL) {
e702ad4
-        Py_DECREF(copied_self);
e702ad4
+        Py_DECREF(obj);
e702ad4
         return NULL;
e702ad4
     }
e702ad4
 
e702ad4
     parg->tag = 'V';
e702ad4
-    stgdict = PyObject_stgdict((PyObject *)copied_self);
e702ad4
+    stgdict = PyObject_stgdict((PyObject *)self);
e702ad4
     assert(stgdict); /* Cannot be NULL for structure/union instances */
e702ad4
     parg->pffi_type = &stgdict->ffi_type_pointer;
e702ad4
-    parg->value.p = copied_self->b_ptr;
e702ad4
-    parg->size = copied_self->b_size;
e702ad4
-    parg->obj = (PyObject *)copied_self;
e702ad4
+    parg->value.p = ptr;
e702ad4
+    parg->size = self->b_size;
e702ad4
+    parg->obj = obj;
e702ad4
     return parg;
e702ad4
 }
e702ad4
 
e702ad4
@@ -5700,6 +5741,10 @@ PyInit__ctypes(void)
e702ad4
     if (PyType_Ready(&DictRemover_Type) < 0)
e702ad4
         return NULL;
e702ad4
 
e702ad4
+    if (PyType_Ready(&StructParam_Type) < 0) {
e702ad4
+        return NULL;
e702ad4
+    }
e702ad4
+
e702ad4
 #ifdef MS_WIN32
e702ad4
     if (create_comerror() < 0)
e702ad4
         return NULL;
e702ad4
-- 
e702ad4
2.21.0
e702ad4