Skip to content

bpo-40795: ctypes calls unraisablehook with an exception #20452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion Lib/ctypes/test/test_callbacks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import functools
import unittest
from test import support

from ctypes import *
from ctypes.test import need_symbol
import _ctypes_test
Expand Down Expand Up @@ -301,8 +303,22 @@ def func(*args):
with self.assertRaises(ArgumentError):
cb(*args2)

def test_convert_result_error(self):
def func():
return ("tuple",)

proto = CFUNCTYPE(c_int)
ctypes_func = proto(func)
with support.catch_unraisable_exception() as cm:
# don't test the result since it is an uninitialized value
result = ctypes_func()

self.assertIsInstance(cm.unraisable.exc_value, TypeError)
self.assertEqual(cm.unraisable.err_msg,
"Exception ignored on converting result "
"of ctypes callback function")
self.assertIs(cm.unraisable.object, func)

################################################################

if __name__ == '__main__':
unittest.main()
51 changes: 27 additions & 24 deletions Lib/ctypes/test/test_random_things.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from ctypes import *
import unittest, sys
import contextlib
from test import support
import unittest
import sys


def callback_func(arg):
42 / arg
Expand Down Expand Up @@ -34,41 +38,40 @@ class CallbackTracbackTestCase(unittest.TestCase):
# created, then a full traceback printed. When SystemExit is
# raised in a callback function, the interpreter exits.

def capture_stderr(self, func, *args, **kw):
# helper - call function 'func', and return the captured stderr
import io
old_stderr = sys.stderr
logger = sys.stderr = io.StringIO()
try:
func(*args, **kw)
finally:
sys.stderr = old_stderr
return logger.getvalue()
@contextlib.contextmanager
def expect_unraisable(self, exc_type, exc_msg=None):
with support.catch_unraisable_exception() as cm:
yield

self.assertIsInstance(cm.unraisable.exc_value, exc_type)
if exc_msg is not None:
self.assertEqual(str(cm.unraisable.exc_value), exc_msg)
self.assertEqual(cm.unraisable.err_msg,
"Exception ignored on calling ctypes "
"callback function")
self.assertIs(cm.unraisable.object, callback_func)

def test_ValueError(self):
cb = CFUNCTYPE(c_int, c_int)(callback_func)
out = self.capture_stderr(cb, 42)
self.assertEqual(out.splitlines()[-1],
"ValueError: 42")
with self.expect_unraisable(ValueError, '42'):
cb(42)

def test_IntegerDivisionError(self):
cb = CFUNCTYPE(c_int, c_int)(callback_func)
out = self.capture_stderr(cb, 0)
self.assertEqual(out.splitlines()[-1][:19],
"ZeroDivisionError: ")
with self.expect_unraisable(ZeroDivisionError):
cb(0)

def test_FloatDivisionError(self):
cb = CFUNCTYPE(c_int, c_double)(callback_func)
out = self.capture_stderr(cb, 0.0)
self.assertEqual(out.splitlines()[-1][:19],
"ZeroDivisionError: ")
with self.expect_unraisable(ZeroDivisionError):
cb(0.0)

def test_TypeErrorDivisionError(self):
cb = CFUNCTYPE(c_int, c_char_p)(callback_func)
out = self.capture_stderr(cb, b"spam")
self.assertEqual(out.splitlines()[-1],
"TypeError: "
"unsupported operand type(s) for /: 'int' and 'bytes'")
err_msg = "unsupported operand type(s) for /: 'int' and 'bytes'"
with self.expect_unraisable(TypeError, err_msg):
cb(b"spam")


if __name__ == '__main__':
unittest.main()
2 changes: 0 additions & 2 deletions Lib/ctypes/test/test_unaligned_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@ class Y(SwappedStructure):
class TestStructures(unittest.TestCase):
def test_native(self):
for typ in structures:
## print typ.value
self.assertEqual(typ.value.offset, 1)
o = typ()
o.value = 4
self.assertEqual(o.value, 4)

def test_swapped(self):
for typ in byteswapped_structures:
## print >> sys.stderr, typ.value
self.assertEqual(typ.value.offset, 1)
o = typ()
o.value = 4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`ctypes` module: If ctypes fails to convert the result of a callback or
if a ctypes callback function raises an exception, sys.unraisablehook is now
called with an exception set. Previously, the error was logged into stderr
by :c:func:`PyErr_Print`.
43 changes: 29 additions & 14 deletions Modules/_ctypes/callbacks.c
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,6 @@ static void _CallPythonObject(void *mem,
pArgs++;
}

#define CHECK(what, x) \
if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyErr_Print()

if (flags & (FUNCFLAG_USE_ERRNO | FUNCFLAG_USE_LASTERROR)) {
error_object = _ctypes_get_errobj(&space);
if (error_object == NULL)
Expand All @@ -235,7 +232,10 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
}

result = PyObject_CallObject(callable, arglist);
CHECK("'calling callback function'", result);
if (result == NULL) {
_PyErr_WriteUnraisableMsg("on calling ctypes callback function",
callable);
}

#ifdef MS_WIN32
if (flags & FUNCFLAG_USE_LASTERROR) {
Expand All @@ -251,16 +251,17 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
}
Py_XDECREF(error_object);

if ((restype != &ffi_type_void) && result) {
PyObject *keep;
if (restype != &ffi_type_void && result) {
assert(setfunc);

#ifdef WORDS_BIGENDIAN
/* See the corresponding code in callproc.c, around line 961 */
if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg))
/* See the corresponding code in _ctypes_callproc():
in callproc.c, around line 1219. */
if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg)) {
mem = (char *)mem + sizeof(ffi_arg) - restype->size;
}
#endif
keep = setfunc(mem, result, 0);
CHECK("'converting callback result'", keep);

/* keep is an object we have to keep alive so that the result
stays valid. If there is no such object, the setfunc will
have returned Py_None.
Expand All @@ -270,18 +271,32 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
be the result. EXCEPT when restype is py_object - Python
itself knows how to manage the refcount of these objects.
*/
if (keep == NULL) /* Could not convert callback result. */
PyErr_WriteUnraisable(callable);
else if (keep == Py_None) /* Nothing to keep */
PyObject *keep = setfunc(mem, result, 0);

if (keep == NULL) {
/* Could not convert callback result. */
_PyErr_WriteUnraisableMsg("on converting result "
"of ctypes callback function",
callable);
}
else if (keep == Py_None) {
/* Nothing to keep */
Py_DECREF(keep);
}
else if (setfunc != _ctypes_get_fielddesc("O")->setfunc) {
if (-1 == PyErr_WarnEx(PyExc_RuntimeWarning,
"memory leak in callback function.",
1))
PyErr_WriteUnraisable(callable);
{
_PyErr_WriteUnraisableMsg("on converting result "
"of ctypes callback function",
callable);
}
}
}

Py_XDECREF(result);

Done:
Py_XDECREF(arglist);
PyGILState_Release(state);
Expand Down
2 changes: 2 additions & 0 deletions Modules/_ctypes/callproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,9 @@ PyObject *_ctypes_callproc(PPROC pProc,
if (rtype->type != FFI_TYPE_FLOAT
&& rtype->type != FFI_TYPE_STRUCT
&& rtype->size < sizeof(ffi_arg))
{
resbuf = (char *)resbuf + sizeof(ffi_arg) - rtype->size;
}
#endif

#ifdef MS_WIN32
Expand Down