bpo-40795: ctypes calls unraisablehook with an exception (GH-20452)

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 PyErr_Print().
This commit is contained in:
Victor Stinner 2020-05-28 00:38:12 +02:00 committed by GitHub
parent e80697d687
commit 10228bad04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 41 deletions

View File

@ -1,5 +1,7 @@
import functools
import unittest
from test import support
from ctypes import *
from ctypes.test import need_symbol
import _ctypes_test
@ -301,8 +303,22 @@ class SampleCallbacksTestCase(unittest.TestCase):
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()

View File

@ -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
@ -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()

View File

@ -27,7 +27,6 @@ for typ in [c_short, c_int, c_long, c_longlong,
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
@ -35,7 +34,6 @@ class TestStructures(unittest.TestCase):
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

View File

@ -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`.

View File

@ -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)
@ -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) {
@ -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.
@ -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);

View File

@ -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